From 2ca7b04acb93c417962e48181284e8800d4508fe Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Feb 2026 15:58:45 +0100 Subject: [PATCH 01/55] feat(core): implement phase 1 and 2 Made with Bob Signed-off-by: Alessandro Pomponio --- orchestrator/core/legacy/__init__.py | 18 +++ orchestrator/core/legacy/metadata.py | 62 ++++++++ orchestrator/core/legacy/registry.py | 139 ++++++++++++++++++ .../core/legacy/validators/__init__.py | 6 + .../validators/discoveryspace/__init__.py | 6 + .../legacy/validators/operation/__init__.py | 6 + .../legacy/validators/resource/__init__.py | 6 + .../resource/entitysource_to_samplestore.py | 44 ++++++ .../legacy/validators/samplestore/__init__.py | 6 + .../samplestore/v1_to_v2_csv_migration.py | 60 ++++++++ 10 files changed, 353 insertions(+) create mode 100644 orchestrator/core/legacy/__init__.py create mode 100644 orchestrator/core/legacy/metadata.py create mode 100644 orchestrator/core/legacy/registry.py create mode 100644 orchestrator/core/legacy/validators/__init__.py create mode 100644 orchestrator/core/legacy/validators/discoveryspace/__init__.py create mode 100644 orchestrator/core/legacy/validators/operation/__init__.py create mode 100644 orchestrator/core/legacy/validators/resource/__init__.py create mode 100644 orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py create mode 100644 orchestrator/core/legacy/validators/samplestore/__init__.py create mode 100644 orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py diff --git a/orchestrator/core/legacy/__init__.py b/orchestrator/core/legacy/__init__.py new file mode 100644 index 000000000..32736960f --- /dev/null +++ b/orchestrator/core/legacy/__init__.py @@ -0,0 +1,18 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validator system for handling deprecated resource formats""" + +from orchestrator.core.legacy.metadata import LegacyValidatorMetadata +from orchestrator.core.legacy.registry import ( + LegacyValidatorRegistry, + legacy_validator, +) + +__all__ = [ + "LegacyValidatorMetadata", + "LegacyValidatorRegistry", + "legacy_validator", +] + +# Made with Bob diff --git a/orchestrator/core/legacy/metadata.py b/orchestrator/core/legacy/metadata.py new file mode 100644 index 000000000..92ba0e3a0 --- /dev/null +++ b/orchestrator/core/legacy/metadata.py @@ -0,0 +1,62 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Metadata models for legacy validators""" + +from collections.abc import Callable +from typing import Annotated + +import pydantic + +from orchestrator.core.resources import CoreResourceKinds + + +class LegacyValidatorMetadata(pydantic.BaseModel): + """Metadata for a legacy validator function""" + + identifier: Annotated[ + str, + pydantic.Field( + description="Unique identifier for this validator (e.g., 'csv_constitutive_columns_migration')" + ), + ] + + resource_type: Annotated[ + CoreResourceKinds, + pydantic.Field(description="Resource type this validator applies to"), + ] + + deprecated_fields: Annotated[ + list[str], + pydantic.Field(description="Fields that this validator handles"), + ] + + deprecated_from_version: Annotated[ + str, + pydantic.Field(description="ADO version when these fields were deprecated"), + ] + + removed_from_version: Annotated[ + str, + pydantic.Field(description="ADO version when automatic upgrade was removed"), + ] + + description: Annotated[ + str, + pydantic.Field( + description="Human-readable description of what this validator does" + ), + ] + + validator_function: Annotated[ + Callable[[dict], dict], + pydantic.Field( + description="The actual migration function", + exclude=True, # Don't serialize the function + ), + ] + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + +# Made with Bob diff --git a/orchestrator/core/legacy/registry.py b/orchestrator/core/legacy/registry.py new file mode 100644 index 000000000..52bc1ad55 --- /dev/null +++ b/orchestrator/core/legacy/registry.py @@ -0,0 +1,139 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Registry for legacy validators that have been removed from active code""" + +from collections.abc import Callable +from functools import wraps +from typing import ClassVar + +from orchestrator.core.legacy.metadata import LegacyValidatorMetadata +from orchestrator.core.resources import CoreResourceKinds + + +class LegacyValidatorRegistry: + """Registry for legacy validators that have been removed from active code""" + + _validators: ClassVar[dict[str, LegacyValidatorMetadata]] = {} + + @classmethod + def register(cls, metadata: LegacyValidatorMetadata) -> None: + """Register a legacy validator + + Args: + metadata: The validator metadata to register + """ + cls._validators[metadata.identifier] = metadata + + @classmethod + def get_validator(cls, identifier: str) -> LegacyValidatorMetadata | None: + """Get a specific validator by identifier + + Args: + identifier: The unique identifier of the validator + + Returns: + The validator metadata if found, None otherwise + """ + return cls._validators.get(identifier) + + @classmethod + def get_validators_for_resource( + cls, resource_type: CoreResourceKinds + ) -> list[LegacyValidatorMetadata]: + """Get all validators for a specific resource type + + Args: + resource_type: The resource type to filter by + + Returns: + List of validator metadata for the specified resource type + """ + return [v for v in cls._validators.values() if v.resource_type == resource_type] + + @classmethod + def find_validators_for_fields( + cls, resource_type: CoreResourceKinds, field_names: list[str] + ) -> list[LegacyValidatorMetadata]: + """Find validators that handle specific deprecated fields + + Args: + resource_type: The resource type to filter by + field_names: List of field names to search for + + Returns: + List of validator metadata that handle any of the specified fields + """ + return [ + v + for v in cls.get_validators_for_resource(resource_type) + if any(field in v.deprecated_fields for field in field_names) + ] + + @classmethod + def list_all(cls) -> list[LegacyValidatorMetadata]: + """List all registered validators + + Returns: + List of all registered validator metadata + """ + return list(cls._validators.values()) + + +def legacy_validator( + identifier: str, + resource_type: CoreResourceKinds, + deprecated_fields: list[str], + deprecated_from_version: str, + removed_from_version: str, + description: str, +) -> Callable[[Callable[[dict], dict]], Callable[[dict], dict]]: + """Decorator to register a legacy validator function + + Args: + identifier: Unique identifier for this validator + resource_type: Resource type this validator applies to + deprecated_fields: Fields that this validator handles + deprecated_from_version: ADO version when these fields were deprecated + removed_from_version: ADO version when automatic upgrade was removed + description: Human-readable description of what this validator does + + Returns: + Decorator function that registers the validator + + Example: + @legacy_validator( + identifier="csv_constitutive_columns_migration", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["constitutivePropertyColumns", "propertyMap"], + deprecated_from_version="1.3.5", + removed_from_version="1.6.0", + description="Migrates CSV sample stores from v1 to v2 format" + ) + def migrate_csv_v1_to_v2(data: dict) -> dict: + # Migration logic here + return data + """ + + def decorator(func: Callable[[dict], dict]) -> Callable[[dict], dict]: + metadata = LegacyValidatorMetadata( + identifier=identifier, + resource_type=resource_type, + deprecated_fields=deprecated_fields, + deprecated_from_version=deprecated_from_version, + removed_from_version=removed_from_version, + description=description, + validator_function=func, + ) + LegacyValidatorRegistry.register(metadata) + + @wraps(func) + def wrapper(*args, **kwargs): # noqa: ANN002, ANN003, ANN202 + return func(*args, **kwargs) + + return wrapper + + return decorator + + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/__init__.py b/orchestrator/core/legacy/validators/__init__.py new file mode 100644 index 000000000..e185d705e --- /dev/null +++ b/orchestrator/core/legacy/validators/__init__.py @@ -0,0 +1,6 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validators for deprecated resource formats""" + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/discoveryspace/__init__.py b/orchestrator/core/legacy/validators/discoveryspace/__init__.py new file mode 100644 index 000000000..7475ec987 --- /dev/null +++ b/orchestrator/core/legacy/validators/discoveryspace/__init__.py @@ -0,0 +1,6 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validators for discovery space migrations""" + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/operation/__init__.py b/orchestrator/core/legacy/validators/operation/__init__.py new file mode 100644 index 000000000..6ccf17a45 --- /dev/null +++ b/orchestrator/core/legacy/validators/operation/__init__.py @@ -0,0 +1,6 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validators for operation migrations""" + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/resource/__init__.py b/orchestrator/core/legacy/validators/resource/__init__.py new file mode 100644 index 000000000..7847cc0ed --- /dev/null +++ b/orchestrator/core/legacy/validators/resource/__init__.py @@ -0,0 +1,6 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validators for generic resource migrations""" + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py new file mode 100644 index 000000000..8f7599ce5 --- /dev/null +++ b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py @@ -0,0 +1,44 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validator for migrating entitysource kind to samplestore kind""" + +from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.resources import CoreResourceKinds + + +@legacy_validator( + identifier="entitysource_to_samplestore", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["kind"], + deprecated_from_version="1.2.0", + removed_from_version="1.5.0", + description="Migrates resources with kind='entitysource' to kind='samplestore'", +) +def migrate_entitysource_to_samplestore(data: dict) -> dict: + """Migrate old entitysource kind to samplestore + + Old format: + kind: "entitysource" + + New format: + kind: "samplestore" + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + # Check if this is an entitysource that needs migration + if data.get("kind") == "entitysource": + data["kind"] = "samplestore" + + return data + + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/samplestore/__init__.py b/orchestrator/core/legacy/validators/samplestore/__init__.py new file mode 100644 index 000000000..439a403d4 --- /dev/null +++ b/orchestrator/core/legacy/validators/samplestore/__init__.py @@ -0,0 +1,6 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validators for sample store migrations""" + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py new file mode 100644 index 000000000..504115645 --- /dev/null +++ b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py @@ -0,0 +1,60 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validator for migrating CSV sample stores from v1 to v2 format""" + +from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.resources import CoreResourceKinds + + +@legacy_validator( + identifier="csv_constitutive_columns_migration", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["constitutivePropertyColumns", "propertyMap"], + deprecated_from_version="1.3.5", + removed_from_version="1.6.0", + description="Migrates CSV sample stores from v1 format (constitutivePropertyColumns at top level) to v2 format (per-experiment constitutivePropertyMap)", +) +def migrate_csv_v1_to_v2(data: dict) -> dict: + """Migrate old CSVSampleStoreDescription format to new format + + Old format: + - constitutivePropertyColumns at top level (list) + - experiments list with propertyMap (not observedPropertyMap) + - No constitutivePropertyMap in experiment descriptions + + New format: + - No constitutivePropertyColumns at top level + - experiments with observedPropertyMap and constitutivePropertyMap + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + # Check if this is old format (has constitutivePropertyColumns at top level) + if "constitutivePropertyColumns" not in data: + return data + + # Extract and remove the top-level constitutivePropertyColumns + constitutive_columns = data.pop("constitutivePropertyColumns") + + # Migrate experiments if present + if "experiments" in data and isinstance(data["experiments"], list): + for exp in data["experiments"]: + if isinstance(exp, dict): + # Rename propertyMap to observedPropertyMap + if "propertyMap" in exp: + exp["observedPropertyMap"] = exp.pop("propertyMap") + # Add constitutivePropertyMap from top-level constitutivePropertyColumns + exp["constitutivePropertyMap"] = constitutive_columns + + return data + + +# Made with Bob From c96b761a4c62f9872c591f3abb0ad1636cef09ff Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Feb 2026 16:32:59 +0100 Subject: [PATCH 02/55] feat(core): finish phase 2 implementation Signed-off-by: Alessandro Pomponio --- orchestrator/cli/commands/upgrade.py | 24 ++++++ orchestrator/cli/models/parameters.py | 2 + orchestrator/cli/utils/legacy/__init__.py | 10 +++ orchestrator/cli/utils/legacy/list.py | 90 ++++++++++++++++++++ orchestrator/cli/utils/resources/handlers.py | 50 +++++++++++ 5 files changed, 176 insertions(+) create mode 100644 orchestrator/cli/utils/legacy/__init__.py create mode 100644 orchestrator/cli/utils/legacy/list.py diff --git a/orchestrator/cli/commands/upgrade.py b/orchestrator/cli/commands/upgrade.py index b18db6f7c..57b67b185 100644 --- a/orchestrator/cli/commands/upgrade.py +++ b/orchestrator/cli/commands/upgrade.py @@ -36,6 +36,20 @@ def upgrade_resource( click_type=HiddenPluralChoice(AdoUpgradeSupportedResourceTypes), ), ], + apply_legacy_validator: Annotated[ + list[str] | None, + typer.Option( + "--apply-legacy-validator", + help="Apply legacy validators by identifier (e.g., 'csv_constitutive_columns_migration'). Can be specified multiple times.", + ), + ] = None, + list_legacy: Annotated[ + bool, + typer.Option( + "--list-legacy", + help="List available legacy validators for this resource type", + ), + ] = False, ) -> None: """ Upgrade resources and contexts. @@ -52,12 +66,22 @@ def upgrade_resource( # Upgrade all operations ado upgrade operations + + # List available legacy validators for sample stores + + ado upgrade sample_stores --list-legacy + + # Apply a legacy validator during upgrade + + ado upgrade sample_stores --apply-legacy-validator csv_constitutive_columns_migration """ ado_configuration: AdoConfiguration = ctx.obj parameters = AdoUpgradeCommandParameters( ado_configuration=ado_configuration, + apply_legacy_validator=apply_legacy_validator, + list_legacy=list_legacy, ) method_mapping = { diff --git a/orchestrator/cli/models/parameters.py b/orchestrator/cli/models/parameters.py index 85fe86a55..0aebb54e6 100644 --- a/orchestrator/cli/models/parameters.py +++ b/orchestrator/cli/models/parameters.py @@ -136,3 +136,5 @@ class AdoTemplateCommandParameters(pydantic.BaseModel): class AdoUpgradeCommandParameters(pydantic.BaseModel): ado_configuration: AdoConfiguration + apply_legacy_validator: list[str] | None = None + list_legacy: bool = False diff --git a/orchestrator/cli/utils/legacy/__init__.py b/orchestrator/cli/utils/legacy/__init__.py new file mode 100644 index 000000000..7413baee2 --- /dev/null +++ b/orchestrator/cli/utils/legacy/__init__.py @@ -0,0 +1,10 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Utilities for working with legacy validators""" + +from orchestrator.cli.utils.legacy.list import list_legacy_validators + +__all__ = ["list_legacy_validators"] + +# Made with Bob diff --git a/orchestrator/cli/utils/legacy/list.py b/orchestrator/cli/utils/legacy/list.py new file mode 100644 index 000000000..2373a5f52 --- /dev/null +++ b/orchestrator/cli/utils/legacy/list.py @@ -0,0 +1,90 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Utilities for listing legacy validators""" + +from rich.console import Console +from rich.panel import Panel + +from orchestrator.core.legacy.registry import LegacyValidatorRegistry +from orchestrator.core.resources import CoreResourceKinds + + +def list_legacy_validators(resource_type: CoreResourceKinds) -> None: + """List all available legacy validators for a specific resource type + + Args: + resource_type: The resource type to list validators for + """ + console = Console() + + # Get validators for this resource type + validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) + + if not validators: + console.print( + f"\n[yellow]No legacy validators available for {resource_type.value}[/yellow]\n" + ) + return + + # Map resource types to their CLI names + resource_name_mapping = { + CoreResourceKinds.SAMPLESTORE: "sample_stores", + CoreResourceKinds.DISCOVERYSPACE: "spaces", + CoreResourceKinds.OPERATION: "operations", + CoreResourceKinds.ACTUATORCONFIGURATION: "actuator_configurations", + CoreResourceKinds.DATACONTAINER: "data_containers", + } + resource_cli_name = resource_name_mapping.get( + resource_type, resource_type.value + "s" + ) + + console.print( + f"\n[bold cyan]Available legacy validators for {resource_cli_name}:[/bold cyan]\n" + ) + + for validator in validators: + # Create a panel for each validator + content_lines = [] + + # Description + content_lines.append("[bold]Description:[/bold]") + content_lines.append(f" {validator.description}") + content_lines.append("") + + # Deprecated fields + content_lines.append("[bold]Handles deprecated fields:[/bold]") + content_lines.extend(f" • {field}" for field in validator.deprecated_fields) + content_lines.append("") + + # Version info + content_lines.append("[bold]Version info:[/bold]") + content_lines.append( + f" Deprecated from: [cyan]{validator.deprecated_from_version}[/cyan]" + ) + content_lines.append( + f" Removed from: [cyan]{validator.removed_from_version}[/cyan]" + ) + content_lines.append("") + + # Usage + content_lines.append("[bold]Usage:[/bold]") + content_lines.append( + f" [green]ado upgrade {resource_cli_name} --apply-legacy-validator {validator.identifier}[/green]" + ) + + panel = Panel( + "\n".join(content_lines), + title=f"[bold magenta]{validator.identifier}[/bold magenta]", + border_style="cyan", + expand=False, + ) + console.print(panel) + console.print() # Add spacing between panels + + console.print( + f"[bold]Found {len(validators)} legacy validator(s) for {resource_cli_name}[/bold]\n" + ) + + +# Made with Bob diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 4c9d0d9bd..65554dfa3 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -235,6 +235,37 @@ def handle_ado_upgrade( parameters: "AdoUpgradeCommandParameters", resource_type: "CoreResourceKinds", ) -> None: + # Handle --list-legacy flag + if parameters.list_legacy: + from orchestrator.cli.utils.legacy import list_legacy_validators + + list_legacy_validators(resource_type) + return + + # Load legacy validators if specified + legacy_validators = [] + if parameters.apply_legacy_validator: + from orchestrator.core.legacy.registry import LegacyValidatorRegistry + + # Import all validator modules to ensure they're registered + _import_legacy_validators() + + for validator_id in parameters.apply_legacy_validator: + validator = LegacyValidatorRegistry.get_validator(validator_id) + if validator is None: + console_print( + f"{ERROR}: Legacy validator '{validator_id}' not found", + stderr=True, + ) + raise typer.Exit(1) + if validator.resource_type != resource_type: + console_print( + f"{ERROR}: Legacy validator '{validator_id}' is for " + f"{validator.resource_type.value}, not {resource_type.value}", + stderr=True, + ) + raise typer.Exit(1) + legacy_validators.append(validator) sql_store = get_sql_store( project_context=parameters.ado_configuration.project_context @@ -246,6 +277,25 @@ def handle_ado_upgrade( for idx, resource in enumerate(resources.values()): status.update(ADO_SPINNER_SAVING_TO_DB + f" ({idx +1 }/{len(resources)})") + + # Apply legacy validators if specified + if legacy_validators: + resource_dict = resource.model_dump(mode="python") + for validator in legacy_validators: + resource_dict = validator.validator_function(resource_dict) + # Reconstruct the resource from the modified dict + resource = type(resource).model_validate(resource_dict) + sql_store.updateResource(resource=resource) console_print(SUCCESS) + + +def _import_legacy_validators() -> None: + """Import all legacy validator modules to ensure they're registered""" + # Import validator modules to trigger decorator registration + try: + import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 + import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 + except ImportError: + pass # Validators may not be available in all installations From 710b69888021c7aeef7dd75d73dfe4336edee9e6 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Feb 2026 16:43:15 +0100 Subject: [PATCH 03/55] feat(core): implement phase 3 Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 115 ++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index 6806944ff..45092de77 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -3,7 +3,9 @@ from typing import NoReturn +import pydantic import typer +from rich.console import Console from orchestrator.cli.utils.output.prints import ( console_print, @@ -12,6 +14,7 @@ no_resource_with_id_in_db_error_str, unknown_experiment_error_str, ) +from orchestrator.core.resources import CoreResourceKinds from orchestrator.metastore.base import ( DeleteFromDatabaseError, NoRelatedResourcesError, @@ -62,3 +65,115 @@ def handle_resource_deletion_error(error: DeleteFromDatabaseError) -> NoReturn: stderr=True, ) raise typer.Exit(1) from error + + +def extract_deprecated_fields_from_validation_error( + error: pydantic.ValidationError, +) -> list[str]: + """Extract field names from pydantic validation errors + + Args: + error: The pydantic validation error + + Returns: + List of field names that caused validation errors + """ + deprecated_fields = [] + for err in error.errors(): + # Get the field path from the error + if err.get("loc"): + # loc is a tuple of field names in the path + field_name = str(err["loc"][0]) + if field_name not in deprecated_fields: + deprecated_fields.append(field_name) + return deprecated_fields + + +def handle_validation_error_with_legacy_suggestions( + error: pydantic.ValidationError, + resource_type: CoreResourceKinds, + resource_identifier: str | None = None, +) -> NoReturn: + """Handle pydantic validation errors and suggest legacy validators if applicable + + Args: + error: The pydantic validation error + resource_type: The type of resource that failed validation + resource_identifier: Optional identifier of the resource + + Raises: + typer.Exit: Always exits with code 1 + """ + from orchestrator.core.legacy.registry import LegacyValidatorRegistry + + # Import validators to ensure they're registered + _import_legacy_validators() + + # Extract field names from validation error + deprecated_fields = extract_deprecated_fields_from_validation_error(error) + + if not deprecated_fields: + # No fields extracted, show standard error + console_print(f"Validation error: {error}", stderr=True) + raise typer.Exit(1) from error + + # Find applicable legacy validators + validators = LegacyValidatorRegistry.find_validators_for_fields( + resource_type=resource_type, field_names=deprecated_fields + ) + + if not validators: + # No legacy validators available, show standard error + console_print(f"Validation error: {error}", stderr=True) + raise typer.Exit(1) from error + + # Display helpful error message with suggestions + console = Console(stderr=True) + resource_id_str = f" '{resource_identifier}'" if resource_identifier else "" + console.print( + f"\n[bold red]Validation Error[/bold red] in {resource_type.value}{resource_id_str}" + ) + console.print( + f"\nDeprecated fields detected: [yellow]{', '.join(deprecated_fields)}[/yellow]" + ) + console.print("\n[bold cyan]Available legacy validators:[/bold cyan]") + + # Map resource types to their CLI names + resource_name_mapping = { + CoreResourceKinds.SAMPLESTORE: "sample_stores", + CoreResourceKinds.DISCOVERYSPACE: "spaces", + CoreResourceKinds.OPERATION: "operations", + CoreResourceKinds.ACTUATORCONFIGURATION: "actuator_configurations", + CoreResourceKinds.DATACONTAINER: "data_containers", + } + resource_cli_name = resource_name_mapping.get( + resource_type, resource_type.value + "s" + ) + + for validator in validators: + console.print(f" • [green]{validator.identifier}[/green]") + console.print(f" {validator.description}") + console.print(f" Handles: {', '.join(validator.deprecated_fields)}") + console.print(f" Deprecated: v{validator.deprecated_from_version}") + console.print() + + console.print("[bold magenta]To upgrade using a legacy validator:[/bold magenta]") + console.print( + f" ado upgrade {resource_cli_name} --apply-legacy-validator {validators[0].identifier}" + ) + console.print() + console.print("[bold magenta]To list all legacy validators:[/bold magenta]") + console.print(f" ado upgrade {resource_cli_name} --list-legacy") + console.print() + + raise typer.Exit(1) from error + + +def _import_legacy_validators() -> None: + """Import all legacy validator modules to ensure they're registered""" + # Import validator modules to trigger decorator registration + try: + import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 + import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 + except ImportError: + pass # Validators may not be available in all installations From 38189cb54684059331e970292628dd61cfef87ed Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Feb 2026 17:14:13 +0100 Subject: [PATCH 04/55] test: add tests Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/legacy/list.py | 13 + tests/core/test_legacy_registry.py | 335 ++++++++++++++++++++ tests/core/test_legacy_validators.py | 428 ++++++++++++++++++++++++++ 3 files changed, 776 insertions(+) create mode 100644 tests/core/test_legacy_registry.py create mode 100644 tests/core/test_legacy_validators.py diff --git a/orchestrator/cli/utils/legacy/list.py b/orchestrator/cli/utils/legacy/list.py index 2373a5f52..ee599fc72 100644 --- a/orchestrator/cli/utils/legacy/list.py +++ b/orchestrator/cli/utils/legacy/list.py @@ -10,6 +10,16 @@ from orchestrator.core.resources import CoreResourceKinds +def _import_legacy_validators() -> None: + """Import all legacy validator modules to ensure they're registered""" + # Import validator modules to trigger decorator registration + try: + import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 + import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 + except ImportError: + pass # Validators may not be available in all installations + + def list_legacy_validators(resource_type: CoreResourceKinds) -> None: """List all available legacy validators for a specific resource type @@ -18,6 +28,9 @@ def list_legacy_validators(resource_type: CoreResourceKinds) -> None: """ console = Console() + # Import all validator modules to ensure they're registered + _import_legacy_validators() + # Get validators for this resource type validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) diff --git a/tests/core/test_legacy_registry.py b/tests/core/test_legacy_registry.py new file mode 100644 index 000000000..ea4d2827e --- /dev/null +++ b/tests/core/test_legacy_registry.py @@ -0,0 +1,335 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Unit tests for the legacy validator registry""" + +from orchestrator.core.legacy.metadata import LegacyValidatorMetadata +from orchestrator.core.legacy.registry import ( + LegacyValidatorRegistry, + legacy_validator, +) +from orchestrator.core.resources import CoreResourceKinds + + +class TestLegacyValidatorMetadata: + """Test the LegacyValidatorMetadata model""" + + def test_create_metadata(self) -> None: + """Test creating validator metadata""" + + def dummy_validator(data: dict) -> dict: + return data + + metadata = LegacyValidatorMetadata( + identifier="test_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1", "field2"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test validator", + validator_function=dummy_validator, + ) + + assert metadata.identifier == "test_validator" + assert metadata.resource_type == CoreResourceKinds.SAMPLESTORE + assert metadata.deprecated_fields == ["field1", "field2"] + assert metadata.deprecated_from_version == "1.0.0" + assert metadata.removed_from_version == "2.0.0" + assert metadata.description == "Test validator" + assert metadata.validator_function == dummy_validator + + def test_metadata_serialization(self) -> None: + """Test that validator function is excluded from serialization""" + + def dummy_validator(data: dict) -> dict: + return data + + metadata = LegacyValidatorMetadata( + identifier="test_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test validator", + validator_function=dummy_validator, + ) + + # Serialize to dict + data = metadata.model_dump() + + # validator_function should be excluded + assert "validator_function" not in data + assert "identifier" in data + assert "resource_type" in data + + +class TestLegacyValidatorRegistry: + """Test the LegacyValidatorRegistry class""" + + def setup_method(self) -> None: + """Clear the registry before each test""" + LegacyValidatorRegistry._validators = {} + + def test_register_validator(self) -> None: + """Test registering a validator""" + + def dummy_validator(data: dict) -> dict: + return data + + metadata = LegacyValidatorMetadata( + identifier="test_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test validator", + validator_function=dummy_validator, + ) + + LegacyValidatorRegistry.register(metadata) + + assert len(LegacyValidatorRegistry._validators) == 1 + assert "test_validator" in LegacyValidatorRegistry._validators + + def test_get_validator(self) -> None: + """Test retrieving a validator by identifier""" + + def dummy_validator(data: dict) -> dict: + return data + + metadata = LegacyValidatorMetadata( + identifier="test_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test validator", + validator_function=dummy_validator, + ) + + LegacyValidatorRegistry.register(metadata) + + retrieved = LegacyValidatorRegistry.get_validator("test_validator") + assert retrieved is not None + assert retrieved.identifier == "test_validator" + + def test_get_nonexistent_validator(self) -> None: + """Test retrieving a validator that doesn't exist""" + retrieved = LegacyValidatorRegistry.get_validator("nonexistent") + assert retrieved is None + + def test_get_validators_for_resource(self) -> None: + """Test retrieving validators for a specific resource type""" + + def dummy_validator(data: dict) -> dict: + return data + + # Register validators for different resource types + metadata1 = LegacyValidatorMetadata( + identifier="samplestore_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Sample store validator", + validator_function=dummy_validator, + ) + + metadata2 = LegacyValidatorMetadata( + identifier="operation_validator", + resource_type=CoreResourceKinds.OPERATION, + deprecated_fields=["field2"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Operation validator", + validator_function=dummy_validator, + ) + + LegacyValidatorRegistry.register(metadata1) + LegacyValidatorRegistry.register(metadata2) + + # Get validators for SAMPLESTORE + samplestore_validators = LegacyValidatorRegistry.get_validators_for_resource( + CoreResourceKinds.SAMPLESTORE + ) + assert len(samplestore_validators) == 1 + assert samplestore_validators[0].identifier == "samplestore_validator" + + # Get validators for OPERATION + operation_validators = LegacyValidatorRegistry.get_validators_for_resource( + CoreResourceKinds.OPERATION + ) + assert len(operation_validators) == 1 + assert operation_validators[0].identifier == "operation_validator" + + def test_find_validators_for_fields(self) -> None: + """Test finding validators that handle specific fields""" + + def dummy_validator(data: dict) -> dict: + return data + + # Register validators with different fields + metadata1 = LegacyValidatorMetadata( + identifier="validator1", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1", "field2"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator 1", + validator_function=dummy_validator, + ) + + metadata2 = LegacyValidatorMetadata( + identifier="validator2", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field3", "field4"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator 2", + validator_function=dummy_validator, + ) + + LegacyValidatorRegistry.register(metadata1) + LegacyValidatorRegistry.register(metadata2) + + # Find validators for field1 + validators = LegacyValidatorRegistry.find_validators_for_fields( + CoreResourceKinds.SAMPLESTORE, ["field1"] + ) + assert len(validators) == 1 + assert validators[0].identifier == "validator1" + + # Find validators for field3 + validators = LegacyValidatorRegistry.find_validators_for_fields( + CoreResourceKinds.SAMPLESTORE, ["field3"] + ) + assert len(validators) == 1 + assert validators[0].identifier == "validator2" + + # Find validators for multiple fields + validators = LegacyValidatorRegistry.find_validators_for_fields( + CoreResourceKinds.SAMPLESTORE, ["field1", "field3"] + ) + assert len(validators) == 2 + + # Find validators for nonexistent field + validators = LegacyValidatorRegistry.find_validators_for_fields( + CoreResourceKinds.SAMPLESTORE, ["nonexistent"] + ) + assert len(validators) == 0 + + def test_list_all(self) -> None: + """Test listing all validators""" + + def dummy_validator(data: dict) -> dict: + return data + + metadata1 = LegacyValidatorMetadata( + identifier="validator1", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator 1", + validator_function=dummy_validator, + ) + + metadata2 = LegacyValidatorMetadata( + identifier="validator2", + resource_type=CoreResourceKinds.OPERATION, + deprecated_fields=["field2"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator 2", + validator_function=dummy_validator, + ) + + LegacyValidatorRegistry.register(metadata1) + LegacyValidatorRegistry.register(metadata2) + + all_validators = LegacyValidatorRegistry.list_all() + assert len(all_validators) == 2 + + +class TestLegacyValidatorDecorator: + """Test the @legacy_validator decorator""" + + def setup_method(self) -> None: + """Clear the registry before each test""" + LegacyValidatorRegistry._validators = {} + + def test_decorator_registers_validator(self) -> None: + """Test that the decorator registers the validator""" + + @legacy_validator( + identifier="test_decorator_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test decorator validator", + ) + def my_validator(data: dict) -> dict: + return data + + # Check that validator was registered + assert len(LegacyValidatorRegistry._validators) == 1 + assert "test_decorator_validator" in LegacyValidatorRegistry._validators + + # Check that the function still works + test_data = {"key": "value"} + result = my_validator(test_data) + assert result == test_data + + def test_decorator_preserves_function_metadata(self) -> None: + """Test that the decorator preserves function metadata""" + + @legacy_validator( + identifier="test_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test validator", + ) + def my_validator(data: dict) -> dict: + """My validator docstring""" + return data + + # Check that function name and docstring are preserved + assert my_validator.__name__ == "my_validator" + assert my_validator.__doc__ == "My validator docstring" + + def test_validator_function_execution(self) -> None: + """Test that the validator function executes correctly""" + + @legacy_validator( + identifier="transform_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["old_field"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Transform validator", + ) + def transform_validator(data: dict) -> dict: + if "old_field" in data: + data["new_field"] = data.pop("old_field") + return data + + # Test the validator function + test_data = {"old_field": "value"} + result = transform_validator(test_data) + assert "old_field" not in result + assert result["new_field"] == "value" + + # Verify it was registered correctly + metadata = LegacyValidatorRegistry.get_validator("transform_validator") + assert metadata is not None + # The validator function should be callable and work correctly + test_data2 = {"old_field": "another_value"} + result2 = metadata.validator_function(test_data2) + assert "old_field" not in result2 + assert result2["new_field"] == "another_value" + + +# Made with Bob diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py new file mode 100644 index 000000000..8fff07207 --- /dev/null +++ b/tests/core/test_legacy_validators.py @@ -0,0 +1,428 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Integration tests for legacy validators with pydantic models and upgrade process""" + +from unittest.mock import MagicMock, patch + +import pydantic +import pytest + +from orchestrator.core.legacy.registry import LegacyValidatorRegistry, legacy_validator +from orchestrator.core.resources import CoreResourceKinds + + +class TestLegacyValidatorWithPydantic: + """Test legacy validators working with pydantic models""" + + def setup_method(self) -> None: + """Clear the registry before each test""" + LegacyValidatorRegistry._validators = {} + + def test_validator_applied_during_model_validation(self) -> None: + """Test that a legacy validator can be manually applied before pydantic validation""" + + # Define a simple pydantic model + class OldModel(pydantic.BaseModel): + new_field: str + + # Register a legacy validator + @legacy_validator( + identifier="old_to_new_field", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["old_field"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Migrate old_field to new_field", + ) + def migrate_old_to_new(data: dict) -> dict: + if "old_field" in data: + data["new_field"] = data.pop("old_field") + return data + + # Get the validator + validator = LegacyValidatorRegistry.get_validator("old_to_new_field") + assert validator is not None + + # Old format data + old_data = {"old_field": "test_value"} + + # Apply legacy validator + migrated_data = validator.validator_function(old_data) + + # Now validate with pydantic + model = OldModel.model_validate(migrated_data) + assert model.new_field == "test_value" + + def test_csv_sample_store_migration_validator(self) -> None: + """Test the CSV sample store migration validator with realistic data""" + + # Import the validator to register it + from orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration import ( # noqa: F401 + migrate_csv_v1_to_v2, + ) + + # Get the validator + validator = LegacyValidatorRegistry.get_validator( + "csv_constitutive_columns_migration" + ) + assert validator is not None + assert validator.resource_type == CoreResourceKinds.SAMPLESTORE + + # Old format CSV sample store data + old_csv_data = { + "kind": "samplestore", + "type": "csv", + "identifier": "test_store", + "identifierColumn": "id", + "constitutivePropertyColumns": ["prop1", "prop2"], + "experiments": [ + { + "experimentIdentifier": "exp1", + "actuatorIdentifier": "act1", + "propertyMap": ["obs1", "obs2"], + } + ], + } + + # Apply the validator + migrated_data = validator.validator_function(old_csv_data.copy()) + + # Verify migration + assert "constitutivePropertyColumns" not in migrated_data + assert len(migrated_data["experiments"]) == 1 + exp = migrated_data["experiments"][0] + assert "propertyMap" not in exp + assert "observedPropertyMap" in exp + assert exp["observedPropertyMap"] == ["obs1", "obs2"] + assert "constitutivePropertyMap" in exp + assert exp["constitutivePropertyMap"] == ["prop1", "prop2"] + + def test_entitysource_to_samplestore_migration(self) -> None: + """Test the entitysource to samplestore kind migration""" + + # Import the validator to register it + from orchestrator.core.legacy.validators.resource.entitysource_to_samplestore import ( # noqa: F401 + migrate_entitysource_to_samplestore, + ) + + # Get the validator + validator = LegacyValidatorRegistry.get_validator("entitysource_to_samplestore") + assert validator is not None + assert validator.resource_type == CoreResourceKinds.SAMPLESTORE + + # Old format with entitysource kind + old_data = { + "kind": "entitysource", + "type": "csv", + "identifier": "test_store", + } + + # Apply the validator + migrated_data = validator.validator_function(old_data.copy()) + + # Verify migration + assert migrated_data["kind"] == "samplestore" + assert migrated_data["type"] == "csv" + assert migrated_data["identifier"] == "test_store" + + def test_chained_validators(self) -> None: + """Test applying multiple validators in sequence""" + + # Register two validators + @legacy_validator( + identifier="step1_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["old_field1"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Step 1 migration", + ) + def step1(data: dict) -> dict: + if "old_field1" in data: + data["intermediate_field"] = data.pop("old_field1") + return data + + @legacy_validator( + identifier="step2_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["intermediate_field"], + deprecated_from_version="2.0.0", + removed_from_version="3.0.0", + description="Step 2 migration", + ) + def step2(data: dict) -> dict: + if "intermediate_field" in data: + data["new_field"] = data.pop("intermediate_field") + return data + + # Get validators + validator1 = LegacyValidatorRegistry.get_validator("step1_validator") + validator2 = LegacyValidatorRegistry.get_validator("step2_validator") + assert validator1 is not None + assert validator2 is not None + + # Old data + old_data = {"old_field1": "value"} + + # Apply validators in sequence + data = validator1.validator_function(old_data) + data = validator2.validator_function(data) + + # Verify final state + assert "old_field1" not in data + assert "intermediate_field" not in data + assert data["new_field"] == "value" + + +class TestUpgradeHandlerIntegration: + """Test the upgrade handler with legacy validators""" + + def setup_method(self) -> None: + """Clear the registry before each test""" + LegacyValidatorRegistry._validators = {} + + def test_upgrade_handler_applies_legacy_validator(self) -> None: + """Test that handle_ado_upgrade applies legacy validators correctly""" + + # Register a test validator + @legacy_validator( + identifier="test_upgrade_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["old_field"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test upgrade validator", + ) + def test_validator(data: dict) -> dict: + if "old_field" in data: + data["new_field"] = data.pop("old_field") + return data + + # Create a mock resource class with model_validate + mock_resource_class = MagicMock() + mock_validated_resource = MagicMock() + mock_resource_class.model_validate.return_value = mock_validated_resource + + # Create mock resource instance + mock_resource = MagicMock() + mock_resource.model_dump.return_value = {"old_field": "test_value"} + # Configure type() to return our mock class + type(mock_resource).model_validate = mock_resource_class.model_validate + + mock_sql_store = MagicMock() + mock_sql_store.getResourcesOfKind.return_value = {"res1": mock_resource} + + # Mock parameters + mock_params = MagicMock() + mock_params.apply_legacy_validator = ["test_upgrade_validator"] + mock_params.list_legacy = False + mock_params.ado_configuration.project_context = "test_context" + + # Patch dependencies + with ( + patch( + "orchestrator.cli.utils.resources.handlers.get_sql_store", + return_value=mock_sql_store, + ), + patch( + "orchestrator.cli.utils.resources.handlers._import_legacy_validators" + ), + patch("orchestrator.cli.utils.resources.handlers.Status"), + patch("orchestrator.cli.utils.resources.handlers.console_print"), + ): + from orchestrator.cli.utils.resources.handlers import ( + handle_ado_upgrade, + ) + + # Call the upgrade handler + handle_ado_upgrade( + parameters=mock_params, + resource_type=CoreResourceKinds.SAMPLESTORE, + ) + + # Verify the resource was processed + mock_resource.model_dump.assert_called_once() + mock_sql_store.updateResource.assert_called_once() + + def test_upgrade_handler_validates_validator_resource_type(self) -> None: + """Test that upgrade handler validates validator resource type matches""" + + # Register a validator for OPERATION + @legacy_validator( + identifier="operation_validator", + resource_type=CoreResourceKinds.OPERATION, + deprecated_fields=["old_field"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Operation validator", + ) + def op_validator(data: dict) -> dict: + return data + + # Mock parameters trying to use operation validator on samplestore + mock_params = MagicMock() + mock_params.apply_legacy_validator = ["operation_validator"] + mock_params.list_legacy = False + mock_params.ado_configuration.project_context = "test_context" + + mock_sql_store = MagicMock() + + # Patch dependencies + with ( + patch( + "orchestrator.cli.utils.resources.handlers.get_sql_store", + return_value=mock_sql_store, + ), + patch( + "orchestrator.cli.utils.resources.handlers._import_legacy_validators" + ), + patch( + "orchestrator.cli.utils.resources.handlers.console_print" + ) as mock_print, + ): + import typer + + from orchestrator.cli.utils.resources.handlers import ( + handle_ado_upgrade, + ) + + # Should raise typer.Exit + with pytest.raises(typer.Exit) as exc_info: + handle_ado_upgrade( + parameters=mock_params, + resource_type=CoreResourceKinds.SAMPLESTORE, + ) + + assert exc_info.value.exit_code == 1 + + # Verify error message was printed + mock_print.assert_called() + call_args = str(mock_print.call_args) + assert "operation_validator" in call_args + assert "operation" in call_args.lower() + assert "samplestore" in call_args.lower() + + def test_upgrade_handler_validates_validator_exists(self) -> None: + """Test that upgrade handler validates validator exists""" + + # Mock parameters with non-existent validator + mock_params = MagicMock() + mock_params.apply_legacy_validator = ["nonexistent_validator"] + mock_params.list_legacy = False + mock_params.ado_configuration.project_context = "test_context" + + mock_sql_store = MagicMock() + + # Patch dependencies + with ( + patch( + "orchestrator.cli.utils.resources.handlers.get_sql_store", + return_value=mock_sql_store, + ), + patch( + "orchestrator.cli.utils.resources.handlers._import_legacy_validators" + ), + patch( + "orchestrator.cli.utils.resources.handlers.console_print" + ) as mock_print, + ): + import typer + + from orchestrator.cli.utils.resources.handlers import ( + handle_ado_upgrade, + ) + + # Should raise typer.Exit + with pytest.raises(typer.Exit) as exc_info: + handle_ado_upgrade( + parameters=mock_params, + resource_type=CoreResourceKinds.SAMPLESTORE, + ) + + assert exc_info.value.exit_code == 1 + + # Verify error message was printed + mock_print.assert_called() + call_args = str(mock_print.call_args) + assert "nonexistent_validator" in call_args + assert "not found" in call_args.lower() + + +class TestValidatorDataIntegrity: + """Test that validators preserve data integrity""" + + def setup_method(self) -> None: + """Clear the registry before each test""" + LegacyValidatorRegistry._validators = {} + + def test_validator_preserves_unrelated_fields(self) -> None: + """Test that validators don't modify unrelated fields""" + + @legacy_validator( + identifier="selective_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["old_field"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Selective validator", + ) + def selective(data: dict) -> dict: + if "old_field" in data: + data["new_field"] = data.pop("old_field") + return data + + validator = LegacyValidatorRegistry.get_validator("selective_validator") + assert validator is not None + + # Data with many fields + data = { + "old_field": "migrate_me", + "keep_field1": "value1", + "keep_field2": 42, + "keep_field3": ["list", "of", "items"], + "keep_field4": {"nested": "dict"}, + } + + result = validator.validator_function(data.copy()) + + # Verify migration happened + assert "old_field" not in result + assert result["new_field"] == "migrate_me" + + # Verify other fields preserved + assert result["keep_field1"] == "value1" + assert result["keep_field2"] == 42 + assert result["keep_field3"] == ["list", "of", "items"] + assert result["keep_field4"] == {"nested": "dict"} + + def test_validator_handles_missing_fields_gracefully(self) -> None: + """Test that validators handle missing deprecated fields gracefully""" + + @legacy_validator( + identifier="graceful_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["optional_old_field"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Graceful validator", + ) + def graceful(data: dict) -> dict: + if "optional_old_field" in data: + data["new_field"] = data.pop("optional_old_field") + return data + + validator = LegacyValidatorRegistry.get_validator("graceful_validator") + assert validator is not None + + # Data without the deprecated field + data = {"other_field": "value"} + + result = validator.validator_function(data.copy()) + + # Should not crash and should preserve data + assert result == data + assert "new_field" not in result + + +# Made with Bob From 2fefd7298323df4449fd65d49e44dc0b7705c34a Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Fri, 27 Feb 2026 13:26:13 +0000 Subject: [PATCH 05/55] feat(cli): add upgrade detection Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/legacy/list.py | 10 + orchestrator/cli/utils/resources/handlers.py | 213 +++++++++++++++--- .../entitysource_to_samplestore.py | 55 +++++ .../properties_field_removal.py | 52 +++++ .../operation/actuators_field_removal.py | 52 +++++ .../randomwalk_mode_to_sampler_config.py | 70 ++++++ .../resource/entitysource_to_samplestore.py | 10 +- .../samplestore/entitysource_migrations.py | 160 +++++++++++++ orchestrator/core/samplestore/config.py | 12 +- orchestrator/metastore/base.py | 20 ++ tests/core/test_legacy_validators.py | 6 +- 11 files changed, 626 insertions(+), 34 deletions(-) create mode 100644 orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py create mode 100644 orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py create mode 100644 orchestrator/core/legacy/validators/operation/actuators_field_removal.py create mode 100644 orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py create mode 100644 orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py diff --git a/orchestrator/cli/utils/legacy/list.py b/orchestrator/cli/utils/legacy/list.py index ee599fc72..d6992308a 100644 --- a/orchestrator/cli/utils/legacy/list.py +++ b/orchestrator/cli/utils/legacy/list.py @@ -14,7 +14,17 @@ def _import_legacy_validators() -> None: """Import all legacy validator modules to ensure they're registered""" # Import validator modules to trigger decorator registration try: + # Discovery Space validators + import orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore # noqa: F401 + import orchestrator.core.legacy.validators.discoveryspace.properties_field_removal # noqa: F401 + + # Operation validators + import orchestrator.core.legacy.validators.operation.actuators_field_removal # noqa: F401 + import orchestrator.core.legacy.validators.operation.randomwalk_mode_to_sampler_config # noqa: F401 + + # Sample Store validators import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 + import orchestrator.core.legacy.validators.samplestore.entitysource_migrations # noqa: F401 import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 except ImportError: pass # Validators may not be available in all installations diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 65554dfa3..5a4f16470 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -235,34 +235,33 @@ def handle_ado_upgrade( parameters: "AdoUpgradeCommandParameters", resource_type: "CoreResourceKinds", ) -> None: + """Upgrade resources, optionally applying legacy validators + + Args: + parameters: Command parameters including legacy validator options + resource_type: The type of resource to upgrade + """ + # Import all validator modules to ensure they're registered + _import_legacy_validators() + # Handle --list-legacy flag if parameters.list_legacy: - from orchestrator.cli.utils.legacy import list_legacy_validators + from orchestrator.cli.utils.legacy.list import list_legacy_validators list_legacy_validators(resource_type) return - # Load legacy validators if specified - legacy_validators = [] + # Get legacy validators if specified + legacy_validators = None if parameters.apply_legacy_validator: from orchestrator.core.legacy.registry import LegacyValidatorRegistry - # Import all validator modules to ensure they're registered - _import_legacy_validators() - + legacy_validators = [] for validator_id in parameters.apply_legacy_validator: validator = LegacyValidatorRegistry.get_validator(validator_id) if validator is None: console_print( - f"{ERROR}: Legacy validator '{validator_id}' not found", - stderr=True, - ) - raise typer.Exit(1) - if validator.resource_type != resource_type: - console_print( - f"{ERROR}: Legacy validator '{validator_id}' is for " - f"{validator.resource_type.value}, not {resource_type.value}", - stderr=True, + f"{ERROR}Unknown legacy validator: {validator_id}", stderr=True ) raise typer.Exit(1) legacy_validators.append(validator) @@ -270,23 +269,52 @@ def handle_ado_upgrade( sql_store = get_sql_store( project_context=parameters.ado_configuration.project_context ) + + # Import resource class mapping for validation + from orchestrator.core import kindmap + with Status(ADO_SPINNER_QUERYING_DB) as status: - resources = sql_store.getResourcesOfKind( - kind=resource_type.value, - ) + # When legacy validators are specified, work with raw data + if legacy_validators: - for idx, resource in enumerate(resources.values()): - status.update(ADO_SPINNER_SAVING_TO_DB + f" ({idx +1 }/{len(resources)})") + identifiers = sql_store.getResourceIdentifiersOfKind( + kind=resource_type.value + ) + + for idx, identifier in enumerate(identifiers): + status.update( + ADO_SPINNER_SAVING_TO_DB + f" ({idx + 1}/{len(identifiers)})" + ) - # Apply legacy validators if specified - if legacy_validators: - resource_dict = resource.model_dump(mode="python") + # Get raw data + resource_dict = sql_store.getResourceRaw(identifier) + if resource_dict is None: + continue + + # Apply legacy validators for validator in legacy_validators: resource_dict = validator.validator_function(resource_dict) - # Reconstruct the resource from the modified dict - resource = type(resource).model_validate(resource_dict) - sql_store.updateResource(resource=resource) + # Validate and save the migrated resource + resource_class = kindmap[resource_type.value] + resource = resource_class.model_validate(resource_dict) + sql_store.updateResource(resource=resource) + else: + # Normal upgrade path without legacy validators + try: + resources = sql_store.getResourcesOfKind( + kind=resource_type.value, ignore_validation_errors=False + ) + except ValueError as err: + # Validation error occurred - check if legacy validators can help + _handle_upgrade_validation_error(err, resource_type, parameters) + raise typer.Exit(1) from err + + for idx, resource in enumerate(resources.values()): + status.update( + ADO_SPINNER_SAVING_TO_DB + f" ({idx + 1}/{len(resources)})" + ) + sql_store.updateResource(resource=resource) console_print(SUCCESS) @@ -295,7 +323,140 @@ def _import_legacy_validators() -> None: """Import all legacy validator modules to ensure they're registered""" # Import validator modules to trigger decorator registration try: + # Discovery Space validators + import orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore # noqa: F401 + import orchestrator.core.legacy.validators.discoveryspace.properties_field_removal # noqa: F401 + + # Operation validators + import orchestrator.core.legacy.validators.operation.actuators_field_removal # noqa: F401 + import orchestrator.core.legacy.validators.operation.randomwalk_mode_to_sampler_config # noqa: F401 + + # Sample Store validators import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 + import orchestrator.core.legacy.validators.samplestore.entitysource_migrations # noqa: F401 import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 except ImportError: pass # Validators may not be available in all installations + + +def _handle_upgrade_validation_error( + error: ValueError, + resource_type: "CoreResourceKinds", + parameters: "AdoUpgradeCommandParameters", +) -> None: + """Handle validation errors during upgrade by suggesting legacy validators + + Analyzes the validation error to extract deprecated field names, finds + applicable legacy validators, and displays helpful suggestions to the user. + + Args: + error: The ValueError containing validation error details + resource_type: The type of resource being upgraded + parameters: The upgrade command parameters + """ + from rich.console import Console + + from orchestrator.core.legacy.registry import LegacyValidatorRegistry + from orchestrator.core.resources import CoreResourceKinds + + console = Console() + + # Import all validator modules to ensure they're registered + _import_legacy_validators() + + # Extract error message + error_msg = str(error) + + # Try to extract deprecated field names from the error message + # The error message contains validation errors with field names + deprecated_fields = [] + + # Look for common patterns in pydantic validation errors + import re + + # Pattern: field_name followed by validation error + field_patterns = [ + r"kind\s*\n\s*Input should be", # kind field + r"moduleType\s*\n\s*Input should be", # moduleType field + r"moduleClass\s*\n\s*", # moduleClass field + r"moduleName\s*\n\s*", # moduleName field + r"constitutivePropertyColumns", # constitutivePropertyColumns field + r"propertyMap", # propertyMap field + r"entitySourceIdentifier", # entitySourceIdentifier field + r"properties\s*\n", # properties field + r"actuators\s*\n", # actuators field + r"mode\s*\n", # mode field (for randomwalk) + ] + + for pattern in field_patterns: + if re.search(pattern, error_msg, re.IGNORECASE): + # Extract the field name from the pattern + field_name = pattern.split(r"\s")[0].split(r"\\")[0] + if field_name not in deprecated_fields: + deprecated_fields.append(field_name) + + # Find applicable legacy validators + validators = [] + if deprecated_fields: + validators = LegacyValidatorRegistry.find_validators_for_fields( + resource_type=resource_type, field_names=deprecated_fields + ) + + # If no validators found by field matching, get all validators for this resource type + if not validators: + validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) + + # Display error message + console.print( + f"\n[bold red]Validation Error[/bold red] while upgrading {resource_type.value} resources" + ) + console.print( + "\n[yellow]Some resources could not be loaded due to deprecated fields or values.[/yellow]" + ) + + if deprecated_fields: + console.print( + f"\nDeprecated fields detected: [yellow]{', '.join(deprecated_fields)}[/yellow]" + ) + + if validators: + console.print( + "\n[bold cyan]Available legacy validators that may help:[/bold cyan]\n" + ) + + # Map resource types to their CLI names + resource_name_mapping = { + CoreResourceKinds.SAMPLESTORE: "samplestore", + CoreResourceKinds.DISCOVERYSPACE: "discoveryspace", + CoreResourceKinds.OPERATION: "operation", + CoreResourceKinds.ACTUATORCONFIGURATION: "actuatorconfiguration", + CoreResourceKinds.DATACONTAINER: "datacontainer", + } + resource_cli_name = resource_name_mapping.get( + resource_type, resource_type.value + ) + + for validator in validators: + console.print(f" • [green]{validator.identifier}[/green]") + console.print(f" {validator.description}") + console.print(f" Handles: {', '.join(validator.deprecated_fields)}") + console.print(f" Deprecated: v{validator.deprecated_from_version}") + console.print() + + console.print( + "[bold magenta]To upgrade using legacy validators:[/bold magenta]" + ) + validator_args = " ".join( + f"--apply-legacy-validator {v.identifier}" for v in validators + ) + console.print(f" ado upgrade {resource_cli_name} {validator_args}") + console.print() + console.print("[bold magenta]To list all legacy validators:[/bold magenta]") + console.print(f" ado upgrade {resource_cli_name} --list-legacy") + else: + console.print( + "\n[yellow]No legacy validators are available for this resource type.[/yellow]" + ) + console.print("The resources may be too old or require manual intervention.") + + console.print() diff --git a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py new file mode 100644 index 000000000..b6b2cf015 --- /dev/null +++ b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py @@ -0,0 +1,55 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validator for renaming entitySourceIdentifier to sampleStoreIdentifier""" + +from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.resources import CoreResourceKinds + + +@legacy_validator( + identifier="discoveryspace_entitysource_to_samplestore", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["entitySourceIdentifier"], + deprecated_from_version="0.9.6", + removed_from_version="1.0.0", + description="Renames 'entitySourceIdentifier' to 'sampleStoreIdentifier' in discovery space configurations", +) +def rename_entitysource_identifier(data: dict) -> dict: + """Rename entitySourceIdentifier to sampleStoreIdentifier + + Old format: + - Used 'entitySourceIdentifier' field + + New format: + - Uses 'sampleStoreIdentifier' field + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + old_key = "entitySourceIdentifier" + new_key = "sampleStoreIdentifier" + + # Check at top level + if old_key in data: + data[new_key] = data.pop(old_key) + + # Also check in config if present + if ( + "config" in data + and isinstance(data["config"], dict) + and old_key in data["config"] + ): + data["config"][new_key] = data["config"].pop(old_key) + + return data + + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py new file mode 100644 index 000000000..eb4822c34 --- /dev/null +++ b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py @@ -0,0 +1,52 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validator for removing deprecated properties field from discovery spaces""" + +from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.resources import CoreResourceKinds + + +@legacy_validator( + identifier="discoveryspace_properties_field_removal", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["properties"], + deprecated_from_version="0.10.1", + removed_from_version="1.0.0", + description="Removes the deprecated 'properties' field from discovery space configurations", +) +def remove_properties_field(data: dict) -> dict: + """Remove deprecated properties field from discovery space configuration + + Old format: + - Had 'properties' field at top level + + New format: + - No 'properties' field + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + # Remove properties field if present + if "properties" in data: + data.pop("properties", None) + + # Also check in config if present + if ( + "config" in data + and isinstance(data["config"], dict) + and "properties" in data["config"] + ): + data["config"].pop("properties", None) + + return data + + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/operation/actuators_field_removal.py b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py new file mode 100644 index 000000000..5b3500424 --- /dev/null +++ b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py @@ -0,0 +1,52 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validator for removing deprecated actuators field from operations""" + +from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.resources import CoreResourceKinds + + +@legacy_validator( + identifier="operation_actuators_field_removal", + resource_type=CoreResourceKinds.OPERATION, + deprecated_fields=["actuators"], + deprecated_from_version="0.9.6", + removed_from_version="1.0.0", + description="Removes the deprecated 'actuators' field from operation configurations. See https://ibm.github.io/ado/resources/operation/#the-operation-configuration-yaml", +) +def remove_actuators_field(data: dict) -> dict: + """Remove deprecated actuators field from operation configuration + + Old format: + - Had 'actuators' field in config + + New format: + - No 'actuators' field (use actuator configurations instead) + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + # Remove actuators field if present at top level + if "actuators" in data: + data.pop("actuators", None) + + # Also check in config if present + if ( + "config" in data + and isinstance(data["config"], dict) + and "actuators" in data["config"] + ): + data["config"].pop("actuators", None) + + return data + + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py new file mode 100644 index 000000000..e815f194b --- /dev/null +++ b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py @@ -0,0 +1,70 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validator for migrating random_walk parameters to samplerConfig""" + +from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.resources import CoreResourceKinds + + +@legacy_validator( + identifier="randomwalk_mode_to_sampler_config", + resource_type=CoreResourceKinds.OPERATION, + deprecated_fields=["mode", "grouping", "samplerType"], + deprecated_from_version="1.0.1", + removed_from_version="1.2", + description="Migrates random_walk parameters from flat structure to nested 'samplerConfig'. See https://ibm.github.io/ado/operators/random-walk/#configuring-a-randomwalk", +) +def migrate_randomwalk_to_sampler_config(data: dict) -> dict: + """Migrate random_walk parameters to samplerConfig structure + + Old format: + - mode, grouping, samplerType at top level of parameters + + New format: + - These fields nested under samplerConfig + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + # Check if this is an operation with parameters that need migration + config = data.get("config") + if not isinstance(config, dict): + return data + + parameters = config.get("parameters") + if not isinstance(parameters, dict): + return data + + # Check if mode field exists (indicator of old format) + if "mode" not in parameters: + return data + + # Extract the old fields + mode = parameters.pop("mode", None) + grouping = parameters.pop("grouping", None) + sampler_type = parameters.pop("samplerType", None) + + # Create samplerConfig if any of the fields were present + if mode is not None or grouping is not None or sampler_type is not None: + sampler_config = {} + if mode is not None: + sampler_config["mode"] = mode + if grouping is not None: + sampler_config["grouping"] = grouping + if sampler_type is not None: + sampler_config["samplerType"] = sampler_type + + parameters["samplerConfig"] = sampler_config + + return data + + +# Made with Bob diff --git a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py index 8f7599ce5..ac21e7bac 100644 --- a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py @@ -8,14 +8,14 @@ @legacy_validator( - identifier="entitysource_to_samplestore", + identifier="samplestore_kind_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, deprecated_fields=["kind"], - deprecated_from_version="1.2.0", - removed_from_version="1.5.0", - description="Migrates resources with kind='entitysource' to kind='samplestore'", + deprecated_from_version="0.9.6", + removed_from_version="1.0.0", + description="Converts resource kind from 'entitysource' to 'samplestore'", ) -def migrate_entitysource_to_samplestore(data: dict) -> dict: +def migrate_entitysource_kind_to_samplestore(data: dict) -> dict: """Migrate old entitysource kind to samplestore Old format: diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py new file mode 100644 index 000000000..2728daa7c --- /dev/null +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -0,0 +1,160 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validators for migrating entitysource to samplestore naming""" + +from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.resources import CoreResourceKinds + + +@legacy_validator( + identifier="samplestore_module_type_entitysource_to_samplestore", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["moduleType"], + deprecated_from_version="0.9.6", + removed_from_version="1.0.0", + description="Converts moduleType value from 'entity_source' to 'sample_store'", +) +def migrate_module_type(data: dict) -> dict: + """Convert moduleType from entity_source to sample_store + + Old format: + - moduleType: "entity_source" + + New format: + - moduleType: "sample_store" + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + def convert_module_type_in_dict(d: dict) -> None: + """Recursively convert moduleType in nested structures""" + if "moduleType" in d and d["moduleType"] == "entity_source": + d["moduleType"] = "sample_store" + + # Check in nested structures + for value in d.values(): + if isinstance(value, dict): + convert_module_type_in_dict(value) + elif isinstance(value, list): + for item in value: + if isinstance(item, dict): + convert_module_type_in_dict(item) + + convert_module_type_in_dict(data) + return data + + +@legacy_validator( + identifier="samplestore_module_class_entitysource_to_samplestore", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["moduleClass"], + deprecated_from_version="0.9.6", + removed_from_version="1.0.0", + description="Converts moduleClass values from EntitySource to SampleStore naming (CSVEntitySource -> CSVSampleStore, SQLEntitySource -> SQLSampleStore)", +) +def migrate_module_class(data: dict) -> dict: + """Convert moduleClass from EntitySource to SampleStore naming + + Old format: + - moduleClass: "CSVEntitySource" or "SQLEntitySource" + + New format: + - moduleClass: "CSVSampleStore" or "SQLSampleStore" + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + value_mappings = { + "CSVEntitySource": "CSVSampleStore", + "SQLEntitySource": "SQLSampleStore", + } + + def convert_module_class_in_dict(d: dict) -> None: + """Recursively convert moduleClass in nested structures""" + if "moduleClass" in d and d["moduleClass"] in value_mappings: + d["moduleClass"] = value_mappings[d["moduleClass"]] + + # Check in nested structures + for value in d.values(): + if isinstance(value, dict): + convert_module_class_in_dict(value) + elif isinstance(value, list): + for item in value: + if isinstance(item, dict): + convert_module_class_in_dict(item) + + convert_module_class_in_dict(data) + return data + + +@legacy_validator( + identifier="samplestore_module_name_entitysource_to_samplestore", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["moduleName"], + deprecated_from_version="0.9.6", + removed_from_version="1.0.0", + description="Updates module paths from entitysource to samplestore (orchestrator.core.entitysource -> orchestrator.core.samplestore)", +) +def migrate_module_name(data: dict) -> dict: + """Convert moduleName paths from entitysource to samplestore + + Old format: + - moduleName: "orchestrator.core.entitysource.*" + - moduleName: "orchestrator.plugins.entitysources.*" + + New format: + - moduleName: "orchestrator.core.samplestore.*" + - moduleName: "orchestrator.plugins.samplestores.*" + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + path_mappings = { + "orchestrator.core.entitysource": "orchestrator.core.samplestore", + "orchestrator.plugins.entitysources": "orchestrator.plugins.samplestores", + } + + def convert_module_name_in_dict(d: dict) -> None: + """Recursively convert moduleName in nested structures""" + if "moduleName" in d and isinstance(d["moduleName"], str): + for old_path, new_path in path_mappings.items(): + if old_path in d["moduleName"]: + d["moduleName"] = d["moduleName"].replace(old_path, new_path) + break + + # Check in nested structures + for value in d.values(): + if isinstance(value, dict): + convert_module_name_in_dict(value) + elif isinstance(value, list): + for item in value: + if isinstance(item, dict): + convert_module_name_in_dict(item) + + convert_module_name_in_dict(data) + return data + + +# Made with Bob diff --git a/orchestrator/core/samplestore/config.py b/orchestrator/core/samplestore/config.py index b284464d3..086bc30d2 100644 --- a/orchestrator/core/samplestore/config.py +++ b/orchestrator/core/samplestore/config.py @@ -76,7 +76,12 @@ def check_is_resource_location_subclass( def check_parameters_valid_for_sample_store_module( cls, parameters: dict, context: pydantic.ValidationInfo ) -> dict: - module = load_module_class_or_function(context.data["module"]) + + module_name = context.data.get("module") + if not module_name: + return parameters + + module = load_module_class_or_function(module_name) validated_parameters = module.validate_parameters(parameters=parameters) # Convert Pydantic model back to dict for serialization if isinstance(validated_parameters, pydantic.BaseModel): @@ -94,6 +99,11 @@ def set_correct_resource_location_class_for_sample_store_module( # However if None is passed explicitly, which would happen on a load of a module which had the "none" default # this method will be called if storageLocation is not None: + + module_name = context.data.get("module") + if not module_name: + return None + sample_store_class = load_module_class_or_function(context.data["module"]) storageLocationClass = sample_store_class.storage_location_class() # 24/04/2025 AP: diff --git a/orchestrator/metastore/base.py b/orchestrator/metastore/base.py index b9d7a9417..8a473b27d 100644 --- a/orchestrator/metastore/base.py +++ b/orchestrator/metastore/base.py @@ -342,6 +342,26 @@ def sample_store_load( storage_location: SQLiteStoreConfiguration | SQLStoreConfiguration, ) -> SampleStoreResource: """Adds storage location information to SQL sample stores""" + # Check for required keys in the nested structure + key_chain = ["config", "specification", "module", "moduleClass"] + current_dict = sample_store_resource_dict + + for i, key in enumerate(key_chain): + if not isinstance(current_dict, dict): + missing_path = ".".join(key_chain[:i]) + raise ValueError( + f"Invalid sample store resource structure: expected dictionary at '{missing_path}', " + f"but got {type(current_dict).__name__}" + ) + + if key not in current_dict: + missing_path = ".".join(key_chain[: i + 1]) + raise ValueError( + f"Invalid sample store resource structure: missing required key '{missing_path}'" + ) + + current_dict = current_dict[key] + if ( sample_store_resource_dict["config"]["specification"]["module"]["moduleClass"] == "SQLSampleStore" diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index 8fff07207..1cfa9f9fb 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -103,11 +103,13 @@ def test_entitysource_to_samplestore_migration(self) -> None: # Import the validator to register it from orchestrator.core.legacy.validators.resource.entitysource_to_samplestore import ( # noqa: F401 - migrate_entitysource_to_samplestore, + migrate_entitysource_kind_to_samplestore, ) # Get the validator - validator = LegacyValidatorRegistry.get_validator("entitysource_to_samplestore") + validator = LegacyValidatorRegistry.get_validator( + "samplestore_kind_entitysource_to_samplestore" + ) assert validator is not None assert validator.resource_type == CoreResourceKinds.SAMPLESTORE From 7cc9195354361db71f8b2c965734a1599159c873 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Fri, 27 Feb 2026 13:28:40 +0000 Subject: [PATCH 06/55] refactor(cli): update examples Signed-off-by: Alessandro Pomponio --- orchestrator/cli/commands/upgrade.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/orchestrator/cli/commands/upgrade.py b/orchestrator/cli/commands/upgrade.py index 57b67b185..d95599106 100644 --- a/orchestrator/cli/commands/upgrade.py +++ b/orchestrator/cli/commands/upgrade.py @@ -40,7 +40,8 @@ def upgrade_resource( list[str] | None, typer.Option( "--apply-legacy-validator", - help="Apply legacy validators by identifier (e.g., 'csv_constitutive_columns_migration'). Can be specified multiple times.", + help="Apply legacy validators by identifier (e.g., 'samplestore_kind_entitysource_to_samplestore'). " + "Can be specified multiple times.", ), ] = None, list_legacy: Annotated[ @@ -69,11 +70,11 @@ def upgrade_resource( # List available legacy validators for sample stores - ado upgrade sample_stores --list-legacy + ado upgrade samplestores --list-legacy # Apply a legacy validator during upgrade - ado upgrade sample_stores --apply-legacy-validator csv_constitutive_columns_migration + ado upgrade samplestores --apply-legacy-validator samplestore_kind_entitysource_to_samplestore """ ado_configuration: AdoConfiguration = ctx.obj From 4ef7b90fe6c9c7d08e814e14eed40d3c4c8ae7ea Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Fri, 27 Feb 2026 13:33:06 +0000 Subject: [PATCH 07/55] refactor(cli): rename list-legacy to list-legacy-validators Signed-off-by: Alessandro Pomponio --- orchestrator/cli/commands/upgrade.py | 8 ++++---- orchestrator/cli/exceptions/handlers.py | 2 +- orchestrator/cli/models/parameters.py | 2 +- orchestrator/cli/utils/resources/handlers.py | 4 ++-- tests/core/test_legacy_validators.py | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/orchestrator/cli/commands/upgrade.py b/orchestrator/cli/commands/upgrade.py index d95599106..ebd283338 100644 --- a/orchestrator/cli/commands/upgrade.py +++ b/orchestrator/cli/commands/upgrade.py @@ -44,10 +44,10 @@ def upgrade_resource( "Can be specified multiple times.", ), ] = None, - list_legacy: Annotated[ + list_legacy_validators: Annotated[ bool, typer.Option( - "--list-legacy", + "--list-legacy-validators", help="List available legacy validators for this resource type", ), ] = False, @@ -70,7 +70,7 @@ def upgrade_resource( # List available legacy validators for sample stores - ado upgrade samplestores --list-legacy + ado upgrade samplestores --list-legacy-validators # Apply a legacy validator during upgrade @@ -82,7 +82,7 @@ def upgrade_resource( parameters = AdoUpgradeCommandParameters( ado_configuration=ado_configuration, apply_legacy_validator=apply_legacy_validator, - list_legacy=list_legacy, + list_legacy_validators=list_legacy_validators, ) method_mapping = { diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index 45092de77..15f1f5159 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -163,7 +163,7 @@ def handle_validation_error_with_legacy_suggestions( ) console.print() console.print("[bold magenta]To list all legacy validators:[/bold magenta]") - console.print(f" ado upgrade {resource_cli_name} --list-legacy") + console.print(f" ado upgrade {resource_cli_name} --list-legacy-validators") console.print() raise typer.Exit(1) from error diff --git a/orchestrator/cli/models/parameters.py b/orchestrator/cli/models/parameters.py index 0aebb54e6..31434c793 100644 --- a/orchestrator/cli/models/parameters.py +++ b/orchestrator/cli/models/parameters.py @@ -137,4 +137,4 @@ class AdoTemplateCommandParameters(pydantic.BaseModel): class AdoUpgradeCommandParameters(pydantic.BaseModel): ado_configuration: AdoConfiguration apply_legacy_validator: list[str] | None = None - list_legacy: bool = False + list_legacy_validators: bool = False diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 5a4f16470..c24e3a303 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -245,7 +245,7 @@ def handle_ado_upgrade( _import_legacy_validators() # Handle --list-legacy flag - if parameters.list_legacy: + if parameters.list_legacy_validators: from orchestrator.cli.utils.legacy.list import list_legacy_validators list_legacy_validators(resource_type) @@ -452,7 +452,7 @@ def _handle_upgrade_validation_error( console.print(f" ado upgrade {resource_cli_name} {validator_args}") console.print() console.print("[bold magenta]To list all legacy validators:[/bold magenta]") - console.print(f" ado upgrade {resource_cli_name} --list-legacy") + console.print(f" ado upgrade {resource_cli_name} --list-legacy-validators") else: console.print( "\n[yellow]No legacy validators are available for this resource type.[/yellow]" diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index 1cfa9f9fb..f8b3f452c 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -218,7 +218,7 @@ def test_validator(data: dict) -> dict: # Mock parameters mock_params = MagicMock() mock_params.apply_legacy_validator = ["test_upgrade_validator"] - mock_params.list_legacy = False + mock_params.list_legacy_validators = False mock_params.ado_configuration.project_context = "test_context" # Patch dependencies @@ -265,7 +265,7 @@ def op_validator(data: dict) -> dict: # Mock parameters trying to use operation validator on samplestore mock_params = MagicMock() mock_params.apply_legacy_validator = ["operation_validator"] - mock_params.list_legacy = False + mock_params.list_legacy_validators = False mock_params.ado_configuration.project_context = "test_context" mock_sql_store = MagicMock() @@ -311,7 +311,7 @@ def test_upgrade_handler_validates_validator_exists(self) -> None: # Mock parameters with non-existent validator mock_params = MagicMock() mock_params.apply_legacy_validator = ["nonexistent_validator"] - mock_params.list_legacy = False + mock_params.list_legacy_validators = False mock_params.ado_configuration.project_context = "test_context" mock_sql_store = MagicMock() From 42a772770cc18c518a566e1038683b9bb46845ce Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Fri, 6 Mar 2026 09:56:49 +0000 Subject: [PATCH 08/55] refactor(cli): use set for field names Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 14 +++----------- orchestrator/cli/utils/resources/handlers.py | 5 ++--- orchestrator/core/legacy/registry.py | 4 ++-- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index 15f1f5159..aa7140c81 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -69,24 +69,16 @@ def handle_resource_deletion_error(error: DeleteFromDatabaseError) -> NoReturn: def extract_deprecated_fields_from_validation_error( error: pydantic.ValidationError, -) -> list[str]: +) -> set[str]: """Extract field names from pydantic validation errors Args: error: The pydantic validation error Returns: - List of field names that caused validation errors + Set of field names that caused validation errors """ - deprecated_fields = [] - for err in error.errors(): - # Get the field path from the error - if err.get("loc"): - # loc is a tuple of field names in the path - field_name = str(err["loc"][0]) - if field_name not in deprecated_fields: - deprecated_fields.append(field_name) - return deprecated_fields + return {str(err["loc"][0]) for err in error.errors() if err.get("loc")} def handle_validation_error_with_legacy_suggestions( diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index c24e3a303..7ac8ba533 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -369,7 +369,7 @@ def _handle_upgrade_validation_error( # Try to extract deprecated field names from the error message # The error message contains validation errors with field names - deprecated_fields = [] + deprecated_fields: set[str] = set() # Look for common patterns in pydantic validation errors import re @@ -392,8 +392,7 @@ def _handle_upgrade_validation_error( if re.search(pattern, error_msg, re.IGNORECASE): # Extract the field name from the pattern field_name = pattern.split(r"\s")[0].split(r"\\")[0] - if field_name not in deprecated_fields: - deprecated_fields.append(field_name) + deprecated_fields.add(field_name) # Find applicable legacy validators validators = [] diff --git a/orchestrator/core/legacy/registry.py b/orchestrator/core/legacy/registry.py index 52bc1ad55..8c830972a 100644 --- a/orchestrator/core/legacy/registry.py +++ b/orchestrator/core/legacy/registry.py @@ -53,13 +53,13 @@ def get_validators_for_resource( @classmethod def find_validators_for_fields( - cls, resource_type: CoreResourceKinds, field_names: list[str] + cls, resource_type: CoreResourceKinds, field_names: set[str] ) -> list[LegacyValidatorMetadata]: """Find validators that handle specific deprecated fields Args: resource_type: The resource type to filter by - field_names: List of field names to search for + field_names: Set of field names to search for Returns: List of validator metadata that handle any of the specified fields From 669e42f07a4116410cee80cf521c48b7b8a21ea9 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Mon, 23 Mar 2026 15:07:58 +0000 Subject: [PATCH 09/55] refactor(cli): remove useless code Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index aa7140c81..88319c094 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -130,17 +130,9 @@ def handle_validation_error_with_legacy_suggestions( ) console.print("\n[bold cyan]Available legacy validators:[/bold cyan]") - # Map resource types to their CLI names - resource_name_mapping = { - CoreResourceKinds.SAMPLESTORE: "sample_stores", - CoreResourceKinds.DISCOVERYSPACE: "spaces", - CoreResourceKinds.OPERATION: "operations", - CoreResourceKinds.ACTUATORCONFIGURATION: "actuator_configurations", - CoreResourceKinds.DATACONTAINER: "data_containers", - } - resource_cli_name = resource_name_mapping.get( - resource_type, resource_type.value + "s" - ) + # Resources can be referenced by their CoreResourceKinds value or by shorthands + # from cli_shorthands_to_cli_names in orchestrator/cli/utils/resources/mappings.py + resource_cli_name = resource_type.value for validator in validators: console.print(f" • [green]{validator.identifier}[/green]") From b1f80769cbed04e0978cb9e94ba7f6e810dfff28 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Mon, 23 Mar 2026 16:15:09 +0000 Subject: [PATCH 10/55] refactor: reduce duplication Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 77 +++----- orchestrator/cli/utils/legacy/common.py | 175 +++++++++++++++++++ orchestrator/cli/utils/legacy/list.py | 37 +--- orchestrator/cli/utils/resources/handlers.py | 145 ++++++--------- tests/core/test_legacy_validators.py | 12 +- 5 files changed, 258 insertions(+), 188 deletions(-) create mode 100644 orchestrator/cli/utils/legacy/common.py diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index 88319c094..e83069bc1 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -67,20 +67,6 @@ def handle_resource_deletion_error(error: DeleteFromDatabaseError) -> NoReturn: raise typer.Exit(1) from error -def extract_deprecated_fields_from_validation_error( - error: pydantic.ValidationError, -) -> set[str]: - """Extract field names from pydantic validation errors - - Args: - error: The pydantic validation error - - Returns: - Set of field names that caused validation errors - """ - return {str(err["loc"][0]) for err in error.errors() if err.get("loc")} - - def handle_validation_error_with_legacy_suggestions( error: pydantic.ValidationError, resource_type: CoreResourceKinds, @@ -96,22 +82,28 @@ def handle_validation_error_with_legacy_suggestions( Raises: typer.Exit: Always exits with code 1 """ + from orchestrator.cli.utils.legacy.common import ( + extract_deprecated_fields_from_validation_error, + import_legacy_validators, + print_validator_suggestions, + ) from orchestrator.core.legacy.registry import LegacyValidatorRegistry # Import validators to ensure they're registered - _import_legacy_validators() - - # Extract field names from validation error - deprecated_fields = extract_deprecated_fields_from_validation_error(error) + import_legacy_validators() - if not deprecated_fields: + # Extract field paths, error details, and leaf field names from validation error + full_field_paths, field_errors, leaf_field_names = ( + extract_deprecated_fields_from_validation_error(error) + ) + if not full_field_paths: # No fields extracted, show standard error console_print(f"Validation error: {error}", stderr=True) raise typer.Exit(1) from error - # Find applicable legacy validators + # Find applicable legacy validators using leaf field names for better matching validators = LegacyValidatorRegistry.find_validators_for_fields( - resource_type=resource_type, field_names=deprecated_fields + resource_type=resource_type, field_names=leaf_field_names ) if not validators: @@ -126,38 +118,21 @@ def handle_validation_error_with_legacy_suggestions( f"\n[bold red]Validation Error[/bold red] in {resource_type.value}{resource_id_str}" ) console.print( - f"\nDeprecated fields detected: [yellow]{', '.join(deprecated_fields)}[/yellow]" + f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(full_field_paths)} field(s)[/yellow]" ) - console.print("\n[bold cyan]Available legacy validators:[/bold cyan]") - - # Resources can be referenced by their CoreResourceKinds value or by shorthands - # from cli_shorthands_to_cli_names in orchestrator/cli/utils/resources/mappings.py - resource_cli_name = resource_type.value - - for validator in validators: - console.print(f" • [green]{validator.identifier}[/green]") - console.print(f" {validator.description}") - console.print(f" Handles: {', '.join(validator.deprecated_fields)}") - console.print(f" Deprecated: v{validator.deprecated_from_version}") - console.print() + # Show detailed error messages for each field path + console.print("\n[bold]Error details:[/bold]") + for field_path in sorted(full_field_paths): + console.print(f" • [cyan]{field_path}[/cyan]:") + for error_msg in field_errors.get(field_path, []): + console.print(f" - {error_msg}") + console.print() - console.print("[bold magenta]To upgrade using a legacy validator:[/bold magenta]") - console.print( - f" ado upgrade {resource_cli_name} --apply-legacy-validator {validators[0].identifier}" + print_validator_suggestions( + validators=validators, + resource_type=resource_type, + console=console, + show_all_validators=False, ) - console.print() - console.print("[bold magenta]To list all legacy validators:[/bold magenta]") - console.print(f" ado upgrade {resource_cli_name} --list-legacy-validators") - console.print() raise typer.Exit(1) from error - - -def _import_legacy_validators() -> None: - """Import all legacy validator modules to ensure they're registered""" - # Import validator modules to trigger decorator registration - try: - import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 - import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 - except ImportError: - pass # Validators may not be available in all installations diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py new file mode 100644 index 000000000..e3d08f362 --- /dev/null +++ b/orchestrator/cli/utils/legacy/common.py @@ -0,0 +1,175 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Common utilities for legacy validator handling""" + +from typing import TYPE_CHECKING + +import pydantic +from rich.console import Console + +if TYPE_CHECKING: + from orchestrator.core.legacy.metadata import LegacyValidatorMetadata + from orchestrator.core.resources import CoreResourceKinds + + +def import_legacy_validators() -> None: + """Import all legacy validator modules to ensure they're registered""" + # Import validator modules to trigger decorator registration + try: + # Discovery Space validators + import orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore # noqa: F401 + import orchestrator.core.legacy.validators.discoveryspace.properties_field_removal # noqa: F401 + + # Operation validators + import orchestrator.core.legacy.validators.operation.actuators_field_removal # noqa: F401 + import orchestrator.core.legacy.validators.operation.randomwalk_mode_to_sampler_config # noqa: F401 + + # Sample Store validators + import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 + import orchestrator.core.legacy.validators.samplestore.entitysource_migrations # noqa: F401 + import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 + except ImportError: + pass # Validators may not be available in all installations + + +def print_validator_suggestions( + validators: list["LegacyValidatorMetadata"], + resource_type: "CoreResourceKinds", + console: Console, + show_all_validators: bool = False, +) -> None: + """Print legacy validator suggestions to the console + + Args: + validators: List of applicable validators + resource_type: The resource type + console: Rich console to print to + show_all_validators: If True, show all validators in the command example + """ + # Resources can be referenced by their CoreResourceKinds value or by shorthands + # from cli_shorthands_to_cli_names in orchestrator/cli/utils/resources/mappings.py + resource_cli_name = resource_type.value + + console.print("\n[bold cyan]Available legacy validators:[/bold cyan]\n") + + for validator in validators: + console.print(f" • [green]{validator.identifier}[/green]") + console.print(f" {validator.description}") + console.print(f" Handles: {', '.join(validator.deprecated_fields)}") + console.print(f" Deprecated: v{validator.deprecated_from_version}") + console.print() + + console.print("[bold magenta]To upgrade using legacy validators:[/bold magenta]") + if show_all_validators: + validator_args = " ".join( + f"--apply-legacy-validator {v.identifier}" for v in validators + ) + else: + validator_args = f"--apply-legacy-validator {validators[0].identifier}" + console.print(f" ado upgrade {resource_cli_name} {validator_args}") + console.print() + console.print("[bold magenta]To list all legacy validators:[/bold magenta]") + console.print(f" ado upgrade {resource_cli_name} --list-legacy-validators") + + +# Made with Bob + + +def extract_deprecated_fields_from_validation_error( + error: pydantic.ValidationError, +) -> tuple[set[str], dict[str, list[str]], set[str]]: + """Extract field names and error details from pydantic validation errors + + Args: + error: The pydantic validation error + + Returns: + Tuple of (full field paths, field error details mapping, leaf field names) + - full field paths: Set of full dotted paths like 'config.specification.module.moduleType' + - field error details: Maps full field path to list of error messages + - leaf field names: Set of just the final field names for validator matching + """ + full_field_paths: set[str] = set() + field_errors: dict[str, list[str]] = {} + leaf_field_names: set[str] = set() + + for err in error.errors(): + if err.get("loc"): + # Build the full dotted path from the location tuple + full_path = ".".join(str(loc) for loc in err["loc"]) + full_field_paths.add(full_path) + + # Get the leaf field name (last element) for validator matching + leaf_field = str(err["loc"][-1]) + leaf_field_names.add(leaf_field) + + # Store the error message for this field path + if full_path not in field_errors: + field_errors[full_path] = [] + + # Build a descriptive error message + msg = err.get("msg", "") + if err.get("input"): + msg = f"{msg} (got: {err['input']})" + + field_errors[full_path].append(msg) + + return full_field_paths, field_errors, leaf_field_names + + +def extract_deprecated_fields_from_value_error( + error: ValueError, +) -> tuple[set[str], dict[str, list[str]], set[str]]: + """Extract field names from ValueError containing pydantic validation errors + + This function attempts to extract the underlying pydantic ValidationError + from a ValueError and extract field names from it. If that fails, it falls + back to regex pattern matching on the error message. + + Args: + error: The ValueError that may contain a pydantic ValidationError + + Returns: + Tuple of (full field paths, field error details mapping, leaf field names) + """ + # Try to extract pydantic ValidationError from the ValueError + if hasattr(error, "__cause__") and isinstance( + error.__cause__, pydantic.ValidationError + ): + return extract_deprecated_fields_from_validation_error(error.__cause__) + + # Fallback to regex pattern matching on error message + import re + + error_msg = str(error) + full_field_paths: set[str] = set() + field_errors: dict[str, list[str]] = {} + leaf_field_names: set[str] = set() + + # Pattern: field_name followed by validation error + field_patterns = [ + r"kind\s*\n\s*Input should be", # kind field + r"moduleType\s*\n\s*Input should be", # moduleType field + r"moduleClass\s*\n\s*", # moduleClass field + r"moduleName\s*\n\s*", # moduleName field + r"constitutivePropertyColumns", # constitutivePropertyColumns field + r"propertyMap", # propertyMap field + r"entitySourceIdentifier", # entitySourceIdentifier field + r"properties\s*\n", # properties field + r"actuators\s*\n", # actuators field + r"mode\s*\n", # mode field (for randomwalk) + ] + + for pattern in field_patterns: + if re.search(pattern, error_msg, re.IGNORECASE): + # Extract the field name from the pattern + field_name = pattern.split(r"\s")[0].split(r"\\")[0] + full_field_paths.add(field_name) + leaf_field_names.add(field_name) + # For regex fallback, we don't have detailed error messages + field_errors[field_name] = [ + "Field validation failed (details in error message)" + ] + + return full_field_paths, field_errors, leaf_field_names diff --git a/orchestrator/cli/utils/legacy/list.py b/orchestrator/cli/utils/legacy/list.py index d6992308a..1a7a84f5a 100644 --- a/orchestrator/cli/utils/legacy/list.py +++ b/orchestrator/cli/utils/legacy/list.py @@ -6,30 +6,11 @@ from rich.console import Console from rich.panel import Panel +from orchestrator.cli.utils.legacy.common import import_legacy_validators from orchestrator.core.legacy.registry import LegacyValidatorRegistry from orchestrator.core.resources import CoreResourceKinds -def _import_legacy_validators() -> None: - """Import all legacy validator modules to ensure they're registered""" - # Import validator modules to trigger decorator registration - try: - # Discovery Space validators - import orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore # noqa: F401 - import orchestrator.core.legacy.validators.discoveryspace.properties_field_removal # noqa: F401 - - # Operation validators - import orchestrator.core.legacy.validators.operation.actuators_field_removal # noqa: F401 - import orchestrator.core.legacy.validators.operation.randomwalk_mode_to_sampler_config # noqa: F401 - - # Sample Store validators - import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 - import orchestrator.core.legacy.validators.samplestore.entitysource_migrations # noqa: F401 - import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 - except ImportError: - pass # Validators may not be available in all installations - - def list_legacy_validators(resource_type: CoreResourceKinds) -> None: """List all available legacy validators for a specific resource type @@ -39,7 +20,7 @@ def list_legacy_validators(resource_type: CoreResourceKinds) -> None: console = Console() # Import all validator modules to ensure they're registered - _import_legacy_validators() + import_legacy_validators() # Get validators for this resource type validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) @@ -50,17 +31,9 @@ def list_legacy_validators(resource_type: CoreResourceKinds) -> None: ) return - # Map resource types to their CLI names - resource_name_mapping = { - CoreResourceKinds.SAMPLESTORE: "sample_stores", - CoreResourceKinds.DISCOVERYSPACE: "spaces", - CoreResourceKinds.OPERATION: "operations", - CoreResourceKinds.ACTUATORCONFIGURATION: "actuator_configurations", - CoreResourceKinds.DATACONTAINER: "data_containers", - } - resource_cli_name = resource_name_mapping.get( - resource_type, resource_type.value + "s" - ) + # Resources can be referenced by their CoreResourceKinds value or by shorthands + # from cli_shorthands_to_cli_names in orchestrator/cli/utils/resources/mappings.py + resource_cli_name = resource_type.value console.print( f"\n[bold cyan]Available legacy validators for {resource_cli_name}:[/bold cyan]\n" diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 7ac8ba533..888100c89 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -1,7 +1,6 @@ # Copyright IBM Corporation 2025, 2026 # SPDX-License-Identifier: MIT - - +import json import pathlib import typing @@ -241,10 +240,12 @@ def handle_ado_upgrade( parameters: Command parameters including legacy validator options resource_type: The type of resource to upgrade """ + from orchestrator.cli.utils.legacy.common import import_legacy_validators + # Import all validator modules to ensure they're registered - _import_legacy_validators() + import_legacy_validators() - # Handle --list-legacy flag + # Handle --list-legacy-validators flag if parameters.list_legacy_validators: from orchestrator.cli.utils.legacy.list import list_legacy_validators @@ -266,6 +267,8 @@ def handle_ado_upgrade( raise typer.Exit(1) legacy_validators.append(validator) + print("selected legacy validators", legacy_validators) + sql_store = get_sql_store( project_context=parameters.ado_configuration.project_context ) @@ -275,13 +278,18 @@ def handle_ado_upgrade( with Status(ADO_SPINNER_QUERYING_DB) as status: # When legacy validators are specified, work with raw data + # print("LEGACY are ", legacy_validators) + # for validator in legacy_validators: + # print(validator.identifier) + if legacy_validators: identifiers = sql_store.getResourceIdentifiersOfKind( kind=resource_type.value ) + # print("Identifiers are", identifiers) - for idx, identifier in enumerate(identifiers): + for idx, identifier in enumerate(identifiers["IDENTIFIER"]): status.update( ADO_SPINNER_SAVING_TO_DB + f" ({idx + 1}/{len(identifiers)})" ) @@ -293,11 +301,18 @@ def handle_ado_upgrade( # Apply legacy validators for validator in legacy_validators: + # print("Applying ", validator.identifier, "\n\n") + # if identifier == 'space-upgrade-test': + # print("From ", json.dumps(resource_dict)) resource_dict = validator.validator_function(resource_dict) + if identifier == "space-upgrade-test": + print("To ", json.dumps(resource_dict)) + raise typer.Exit(1) # Validate and save the migrated resource resource_class = kindmap[resource_type.value] resource = resource_class.model_validate(resource_dict) + # raise Exception sql_store.updateResource(resource=resource) else: # Normal upgrade path without legacy validators @@ -319,26 +334,6 @@ def handle_ado_upgrade( console_print(SUCCESS) -def _import_legacy_validators() -> None: - """Import all legacy validator modules to ensure they're registered""" - # Import validator modules to trigger decorator registration - try: - # Discovery Space validators - import orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore # noqa: F401 - import orchestrator.core.legacy.validators.discoveryspace.properties_field_removal # noqa: F401 - - # Operation validators - import orchestrator.core.legacy.validators.operation.actuators_field_removal # noqa: F401 - import orchestrator.core.legacy.validators.operation.randomwalk_mode_to_sampler_config # noqa: F401 - - # Sample Store validators - import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 - import orchestrator.core.legacy.validators.samplestore.entitysource_migrations # noqa: F401 - import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 - except ImportError: - pass # Validators may not be available in all installations - - def _handle_upgrade_validation_error( error: ValueError, resource_type: "CoreResourceKinds", @@ -356,49 +351,28 @@ def _handle_upgrade_validation_error( """ from rich.console import Console + from orchestrator.cli.utils.legacy.common import ( + extract_deprecated_fields_from_value_error, + import_legacy_validators, + print_validator_suggestions, + ) from orchestrator.core.legacy.registry import LegacyValidatorRegistry - from orchestrator.core.resources import CoreResourceKinds console = Console() # Import all validator modules to ensure they're registered - _import_legacy_validators() - - # Extract error message - error_msg = str(error) - - # Try to extract deprecated field names from the error message - # The error message contains validation errors with field names - deprecated_fields: set[str] = set() - - # Look for common patterns in pydantic validation errors - import re - - # Pattern: field_name followed by validation error - field_patterns = [ - r"kind\s*\n\s*Input should be", # kind field - r"moduleType\s*\n\s*Input should be", # moduleType field - r"moduleClass\s*\n\s*", # moduleClass field - r"moduleName\s*\n\s*", # moduleName field - r"constitutivePropertyColumns", # constitutivePropertyColumns field - r"propertyMap", # propertyMap field - r"entitySourceIdentifier", # entitySourceIdentifier field - r"properties\s*\n", # properties field - r"actuators\s*\n", # actuators field - r"mode\s*\n", # mode field (for randomwalk) - ] - - for pattern in field_patterns: - if re.search(pattern, error_msg, re.IGNORECASE): - # Extract the field name from the pattern - field_name = pattern.split(r"\s")[0].split(r"\\")[0] - deprecated_fields.add(field_name) - - # Find applicable legacy validators + import_legacy_validators() + + # Extract field paths, error details, and leaf field names from the error + full_field_paths, field_errors, leaf_field_names = ( + extract_deprecated_fields_from_value_error(error) + ) + + # Find applicable legacy validators using leaf field names for better matching validators = [] - if deprecated_fields: + if leaf_field_names: validators = LegacyValidatorRegistry.find_validators_for_fields( - resource_type=resource_type, field_names=deprecated_fields + resource_type=resource_type, field_names=leaf_field_names ) # If no validators found by field matching, get all validators for this resource type @@ -410,48 +384,27 @@ def _handle_upgrade_validation_error( f"\n[bold red]Validation Error[/bold red] while upgrading {resource_type.value} resources" ) console.print( - "\n[yellow]Some resources could not be loaded due to deprecated fields or values.[/yellow]" + "\n[yellow]Some resources could not be loaded due to validation errors.[/yellow]" ) - if deprecated_fields: + if full_field_paths: console.print( - f"\nDeprecated fields detected: [yellow]{', '.join(deprecated_fields)}[/yellow]" + f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(full_field_paths)} field(s)[/yellow]" ) + # Show detailed error messages for each field path + console.print("\n[bold]Error details:[/bold]") + for field_path in sorted(full_field_paths): + console.print(f" • [cyan]{field_path}[/cyan]:") + for error_msg in field_errors.get(field_path, []): + console.print(f" - {error_msg}") if validators: - console.print( - "\n[bold cyan]Available legacy validators that may help:[/bold cyan]\n" - ) - - # Map resource types to their CLI names - resource_name_mapping = { - CoreResourceKinds.SAMPLESTORE: "samplestore", - CoreResourceKinds.DISCOVERYSPACE: "discoveryspace", - CoreResourceKinds.OPERATION: "operation", - CoreResourceKinds.ACTUATORCONFIGURATION: "actuatorconfiguration", - CoreResourceKinds.DATACONTAINER: "datacontainer", - } - resource_cli_name = resource_name_mapping.get( - resource_type, resource_type.value - ) - - for validator in validators: - console.print(f" • [green]{validator.identifier}[/green]") - console.print(f" {validator.description}") - console.print(f" Handles: {', '.join(validator.deprecated_fields)}") - console.print(f" Deprecated: v{validator.deprecated_from_version}") - console.print() - - console.print( - "[bold magenta]To upgrade using legacy validators:[/bold magenta]" - ) - validator_args = " ".join( - f"--apply-legacy-validator {v.identifier}" for v in validators + print_validator_suggestions( + validators=validators, + resource_type=resource_type, + console=console, + show_all_validators=True, ) - console.print(f" ado upgrade {resource_cli_name} {validator_args}") - console.print() - console.print("[bold magenta]To list all legacy validators:[/bold magenta]") - console.print(f" ado upgrade {resource_cli_name} --list-legacy-validators") else: console.print( "\n[yellow]No legacy validators are available for this resource type.[/yellow]" diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index f8b3f452c..14b52bea3 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -227,9 +227,7 @@ def test_validator(data: dict) -> dict: "orchestrator.cli.utils.resources.handlers.get_sql_store", return_value=mock_sql_store, ), - patch( - "orchestrator.cli.utils.resources.handlers._import_legacy_validators" - ), + patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), patch("orchestrator.cli.utils.resources.handlers.Status"), patch("orchestrator.cli.utils.resources.handlers.console_print"), ): @@ -276,9 +274,7 @@ def op_validator(data: dict) -> dict: "orchestrator.cli.utils.resources.handlers.get_sql_store", return_value=mock_sql_store, ), - patch( - "orchestrator.cli.utils.resources.handlers._import_legacy_validators" - ), + patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), patch( "orchestrator.cli.utils.resources.handlers.console_print" ) as mock_print, @@ -322,9 +318,7 @@ def test_upgrade_handler_validates_validator_exists(self) -> None: "orchestrator.cli.utils.resources.handlers.get_sql_store", return_value=mock_sql_store, ), - patch( - "orchestrator.cli.utils.resources.handlers._import_legacy_validators" - ), + patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), patch( "orchestrator.cli.utils.resources.handlers.console_print" ) as mock_print, From 22aac8ee493b2287fcf8487e7949ee984e570948 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Mon, 23 Mar 2026 16:27:47 +0000 Subject: [PATCH 11/55] refactor(cli): do not hardcode field patterns Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/legacy/common.py | 37 ++++++++------------ orchestrator/cli/utils/resources/handlers.py | 2 +- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index e3d08f362..8d338273a 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -120,15 +120,18 @@ def extract_deprecated_fields_from_validation_error( def extract_deprecated_fields_from_value_error( error: ValueError, + resource_type: "CoreResourceKinds", ) -> tuple[set[str], dict[str, list[str]], set[str]]: """Extract field names from ValueError containing pydantic validation errors This function attempts to extract the underlying pydantic ValidationError from a ValueError and extract field names from it. If that fails, it falls - back to regex pattern matching on the error message. + back to simple string matching on the error message using known deprecated + fields from the legacy validator registry. Args: error: The ValueError that may contain a pydantic ValidationError + resource_type: The resource type to get deprecated fields for Returns: Tuple of (full field paths, field error details mapping, leaf field names) @@ -139,35 +142,25 @@ def extract_deprecated_fields_from_value_error( ): return extract_deprecated_fields_from_validation_error(error.__cause__) - # Fallback to regex pattern matching on error message - import re + # Fallback to simple string matching on error message + from orchestrator.core.legacy.registry import LegacyValidatorRegistry error_msg = str(error) full_field_paths: set[str] = set() field_errors: dict[str, list[str]] = {} leaf_field_names: set[str] = set() - # Pattern: field_name followed by validation error - field_patterns = [ - r"kind\s*\n\s*Input should be", # kind field - r"moduleType\s*\n\s*Input should be", # moduleType field - r"moduleClass\s*\n\s*", # moduleClass field - r"moduleName\s*\n\s*", # moduleName field - r"constitutivePropertyColumns", # constitutivePropertyColumns field - r"propertyMap", # propertyMap field - r"entitySourceIdentifier", # entitySourceIdentifier field - r"properties\s*\n", # properties field - r"actuators\s*\n", # actuators field - r"mode\s*\n", # mode field (for randomwalk) - ] - - for pattern in field_patterns: - if re.search(pattern, error_msg, re.IGNORECASE): - # Extract the field name from the pattern - field_name = pattern.split(r"\s")[0].split(r"\\")[0] + # Get all deprecated field names from registered validators for this resource type + validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) + deprecated_field_names = { + field for validator in validators for field in validator.deprecated_fields + } + + for field_name in deprecated_field_names: + if field_name in error_msg: full_field_paths.add(field_name) leaf_field_names.add(field_name) - # For regex fallback, we don't have detailed error messages + # For string matching fallback, we don't have detailed error messages field_errors[field_name] = [ "Field validation failed (details in error message)" ] diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 888100c89..92e046817 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -365,7 +365,7 @@ def _handle_upgrade_validation_error( # Extract field paths, error details, and leaf field names from the error full_field_paths, field_errors, leaf_field_names = ( - extract_deprecated_fields_from_value_error(error) + extract_deprecated_fields_from_value_error(error, resource_type) ) # Find applicable legacy validators using leaf field names for better matching From 7cf226656022d30f2060d0519e5c6736fcc60881 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 10:39:48 +0000 Subject: [PATCH 12/55] refactor(cli): remove debug code Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/resources/handlers.py | 22 +++++++------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 92e046817..ff50dbca5 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -1,6 +1,6 @@ # Copyright IBM Corporation 2025, 2026 # SPDX-License-Identifier: MIT -import json +import logging import pathlib import typing @@ -35,6 +35,8 @@ from orchestrator.metastore.base import ResourceDoesNotExistError from orchestrator.utilities.rich import dataframe_to_rich_table +logger = logging.getLogger(__name__) + if typing.TYPE_CHECKING: from orchestrator.cli.models.parameters import ( AdoGetCommandParameters, @@ -267,7 +269,9 @@ def handle_ado_upgrade( raise typer.Exit(1) legacy_validators.append(validator) - print("selected legacy validators", legacy_validators) + logger.debug( + f"Selected legacy validators: {[v.identifier for v in legacy_validators]}" + ) sql_store = get_sql_store( project_context=parameters.ado_configuration.project_context @@ -278,16 +282,11 @@ def handle_ado_upgrade( with Status(ADO_SPINNER_QUERYING_DB) as status: # When legacy validators are specified, work with raw data - # print("LEGACY are ", legacy_validators) - # for validator in legacy_validators: - # print(validator.identifier) - if legacy_validators: identifiers = sql_store.getResourceIdentifiersOfKind( kind=resource_type.value ) - # print("Identifiers are", identifiers) for idx, identifier in enumerate(identifiers["IDENTIFIER"]): status.update( @@ -301,18 +300,13 @@ def handle_ado_upgrade( # Apply legacy validators for validator in legacy_validators: - # print("Applying ", validator.identifier, "\n\n") - # if identifier == 'space-upgrade-test': - # print("From ", json.dumps(resource_dict)) + logger.debug(f"Applying validator: {validator.identifier}") resource_dict = validator.validator_function(resource_dict) - if identifier == "space-upgrade-test": - print("To ", json.dumps(resource_dict)) - raise typer.Exit(1) + logger.debug(f"Validator {validator.identifier} completed") # Validate and save the migrated resource resource_class = kindmap[resource_type.value] resource = resource_class.model_validate(resource_dict) - # raise Exception sql_store.updateResource(resource=resource) else: # Normal upgrade path without legacy validators From 0bbac9944f32ec84ee846c862e9c29d4d9837cb8 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 10:40:50 +0000 Subject: [PATCH 13/55] fix(legacy): avoid modifying wrong fields in legacy validators Signed-off-by: Alessandro Pomponio --- .../entitysource_to_samplestore.py | 16 +++--- .../properties_field_removal.py | 22 ++++---- .../operation/actuators_field_removal.py | 16 +++--- .../samplestore/entitysource_migrations.py | 55 +++++++++++++++---- .../samplestore/v1_to_v2_csv_migration.py | 40 +++++++++----- 5 files changed, 99 insertions(+), 50 deletions(-) diff --git a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py index b6b2cf015..0a53f260b 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py @@ -18,11 +18,17 @@ def rename_entitysource_identifier(data: dict) -> dict: """Rename entitySourceIdentifier to sampleStoreIdentifier + The 'entitySourceIdentifier' field was renamed to 'sampleStoreIdentifier' in config. + This validator operates only on the config level, matching the original + pydantic validator behavior. + Old format: - - Used 'entitySourceIdentifier' field + config: + entitySourceIdentifier: "store-id" New format: - - Uses 'sampleStoreIdentifier' field + config: + sampleStoreIdentifier: "store-id" Args: data: The resource data dictionary @@ -37,11 +43,7 @@ def rename_entitysource_identifier(data: dict) -> dict: old_key = "entitySourceIdentifier" new_key = "sampleStoreIdentifier" - # Check at top level - if old_key in data: - data[new_key] = data.pop(old_key) - - # Also check in config if present + # Only check in config (where the field actually exists) if ( "config" in data and isinstance(data["config"], dict) diff --git a/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py index eb4822c34..0c7308d18 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py +++ b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py @@ -18,11 +18,17 @@ def remove_properties_field(data: dict) -> dict: """Remove deprecated properties field from discovery space configuration + The 'properties' field was deprecated in config and should be removed. + This validator operates only on the config level, matching the original + pydantic validator behavior. + Old format: - - Had 'properties' field at top level + config: + properties: [...] New format: - - No 'properties' field + config: + # No properties field Args: data: The resource data dictionary @@ -34,16 +40,8 @@ def remove_properties_field(data: dict) -> dict: if not isinstance(data, dict): return data - # Remove properties field if present - if "properties" in data: - data.pop("properties", None) - - # Also check in config if present - if ( - "config" in data - and isinstance(data["config"], dict) - and "properties" in data["config"] - ): + # Only check in config (where the field actually exists) + if "config" in data and isinstance(data["config"], dict): data["config"].pop("properties", None) return data diff --git a/orchestrator/core/legacy/validators/operation/actuators_field_removal.py b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py index 5b3500424..bafc618ca 100644 --- a/orchestrator/core/legacy/validators/operation/actuators_field_removal.py +++ b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py @@ -18,11 +18,17 @@ def remove_actuators_field(data: dict) -> dict: """Remove deprecated actuators field from operation configuration + The 'actuators' field was deprecated in config and should be removed. + This validator operates only on the config level, matching the original + pydantic validator behavior. + Old format: - - Had 'actuators' field in config + config: + actuators: [...] New format: - - No 'actuators' field (use actuator configurations instead) + config: + # No actuators field (use actuator configurations instead) Args: data: The resource data dictionary @@ -34,11 +40,7 @@ def remove_actuators_field(data: dict) -> dict: if not isinstance(data, dict): return data - # Remove actuators field if present at top level - if "actuators" in data: - data.pop("actuators", None) - - # Also check in config if present + # Only check in config (where the field actually exists) if ( "config" in data and isinstance(data["config"], dict) diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index 2728daa7c..f94b58f42 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -18,11 +18,17 @@ def migrate_module_type(data: dict) -> dict: """Convert moduleType from entity_source to sample_store + This validator recursively searches for moduleType fields within the config + and converts them from 'entity_source' to 'sample_store'. It operates only + within the config level, matching the original pydantic validator behavior. + Old format: - - moduleType: "entity_source" + config: + moduleType: "entity_source" New format: - - moduleType: "sample_store" + config: + moduleType: "sample_store" Args: data: The resource data dictionary @@ -34,6 +40,10 @@ def migrate_module_type(data: dict) -> dict: if not isinstance(data, dict): return data + # Only operate within config + if "config" not in data or not isinstance(data["config"], dict): + return data + def convert_module_type_in_dict(d: dict) -> None: """Recursively convert moduleType in nested structures""" if "moduleType" in d and d["moduleType"] == "entity_source": @@ -48,7 +58,8 @@ def convert_module_type_in_dict(d: dict) -> None: if isinstance(item, dict): convert_module_type_in_dict(item) - convert_module_type_in_dict(data) + # Start recursion from config level + convert_module_type_in_dict(data["config"]) return data @@ -63,11 +74,17 @@ def convert_module_type_in_dict(d: dict) -> None: def migrate_module_class(data: dict) -> dict: """Convert moduleClass from EntitySource to SampleStore naming + This validator recursively searches for moduleClass fields within the config + and converts them from EntitySource to SampleStore naming. It operates only + within the config level, matching the original pydantic validator behavior. + Old format: - - moduleClass: "CSVEntitySource" or "SQLEntitySource" + config: + moduleClass: "CSVEntitySource" or "SQLEntitySource" New format: - - moduleClass: "CSVSampleStore" or "SQLSampleStore" + config: + moduleClass: "CSVSampleStore" or "SQLSampleStore" Args: data: The resource data dictionary @@ -79,6 +96,10 @@ def migrate_module_class(data: dict) -> dict: if not isinstance(data, dict): return data + # Only operate within config + if "config" not in data or not isinstance(data["config"], dict): + return data + value_mappings = { "CSVEntitySource": "CSVSampleStore", "SQLEntitySource": "SQLSampleStore", @@ -98,7 +119,8 @@ def convert_module_class_in_dict(d: dict) -> None: if isinstance(item, dict): convert_module_class_in_dict(item) - convert_module_class_in_dict(data) + # Start recursion from config level + convert_module_class_in_dict(data["config"]) return data @@ -113,13 +135,19 @@ def convert_module_class_in_dict(d: dict) -> None: def migrate_module_name(data: dict) -> dict: """Convert moduleName paths from entitysource to samplestore + This validator recursively searches for moduleName fields within the config + and converts paths from entitysource to samplestore. It operates only + within the config level, matching the original pydantic validator behavior. + Old format: - - moduleName: "orchestrator.core.entitysource.*" - - moduleName: "orchestrator.plugins.entitysources.*" + config: + moduleName: "orchestrator.core.entitysource.*" + moduleName: "orchestrator.plugins.entitysources.*" New format: - - moduleName: "orchestrator.core.samplestore.*" - - moduleName: "orchestrator.plugins.samplestores.*" + config: + moduleName: "orchestrator.core.samplestore.*" + moduleName: "orchestrator.plugins.samplestores.*" Args: data: The resource data dictionary @@ -131,6 +159,10 @@ def migrate_module_name(data: dict) -> dict: if not isinstance(data, dict): return data + # Only operate within config + if "config" not in data or not isinstance(data["config"], dict): + return data + path_mappings = { "orchestrator.core.entitysource": "orchestrator.core.samplestore", "orchestrator.plugins.entitysources": "orchestrator.plugins.samplestores", @@ -153,7 +185,8 @@ def convert_module_name_in_dict(d: dict) -> None: if isinstance(item, dict): convert_module_name_in_dict(item) - convert_module_name_in_dict(data) + # Start recursion from config level + convert_module_name_in_dict(data["config"]) return data diff --git a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py index 504115645..a51b6f681 100644 --- a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py +++ b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py @@ -18,14 +18,22 @@ def migrate_csv_v1_to_v2(data: dict) -> dict: """Migrate old CSVSampleStoreDescription format to new format + This validator operates on the config level, migrating CSV sample store + configurations from v1 to v2 format. It matches the original pydantic + validator behavior. + Old format: - - constitutivePropertyColumns at top level (list) - - experiments list with propertyMap (not observedPropertyMap) - - No constitutivePropertyMap in experiment descriptions + config: + constitutivePropertyColumns: [...] # at config level + experiments: + - propertyMap: {...} New format: - - No constitutivePropertyColumns at top level - - experiments with observedPropertyMap and constitutivePropertyMap + config: + # No constitutivePropertyColumns at config level + experiments: + - observedPropertyMap: {...} + constitutivePropertyMap: [...] Args: data: The resource data dictionary @@ -37,21 +45,27 @@ def migrate_csv_v1_to_v2(data: dict) -> dict: if not isinstance(data, dict): return data - # Check if this is old format (has constitutivePropertyColumns at top level) - if "constitutivePropertyColumns" not in data: + # Only operate within config + if "config" not in data or not isinstance(data["config"], dict): + return data + + config = data["config"] + + # Check if this is old format (has constitutivePropertyColumns in config) + if "constitutivePropertyColumns" not in config: return data - # Extract and remove the top-level constitutivePropertyColumns - constitutive_columns = data.pop("constitutivePropertyColumns") + # Extract and remove the constitutivePropertyColumns from config + constitutive_columns = config.pop("constitutivePropertyColumns") - # Migrate experiments if present - if "experiments" in data and isinstance(data["experiments"], list): - for exp in data["experiments"]: + # Migrate experiments if present in config + if "experiments" in config and isinstance(config["experiments"], list): + for exp in config["experiments"]: if isinstance(exp, dict): # Rename propertyMap to observedPropertyMap if "propertyMap" in exp: exp["observedPropertyMap"] = exp.pop("propertyMap") - # Add constitutivePropertyMap from top-level constitutivePropertyColumns + # Add constitutivePropertyMap from config-level constitutivePropertyColumns exp["constitutivePropertyMap"] = constitutive_columns return data From 1ed3874bc4c42fe2224d16d237be0fae4c0d2e5f Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 10:53:30 +0000 Subject: [PATCH 14/55] feat(cli): add transaction safety to ado upgrade of legacy resources Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/resources/handlers.py | 66 +++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index ff50dbca5..a3b3641c5 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -288,9 +288,15 @@ def handle_ado_upgrade( kind=resource_type.value ) + # Phase 1: Collect and validate all migrations (transaction safety) + # Validate all resources before saving any to ensure atomicity + migrations = [] + resource_class = kindmap[resource_type.value] + for idx, identifier in enumerate(identifiers["IDENTIFIER"]): status.update( - ADO_SPINNER_SAVING_TO_DB + f" ({idx + 1}/{len(identifiers)})" + ADO_SPINNER_QUERYING_DB + + f" - Validating ({idx + 1}/{len(identifiers)})" ) # Get raw data @@ -299,15 +305,55 @@ def handle_ado_upgrade( continue # Apply legacy validators - for validator in legacy_validators: - logger.debug(f"Applying validator: {validator.identifier}") - resource_dict = validator.validator_function(resource_dict) - logger.debug(f"Validator {validator.identifier} completed") - - # Validate and save the migrated resource - resource_class = kindmap[resource_type.value] - resource = resource_class.model_validate(resource_dict) - sql_store.updateResource(resource=resource) + try: + for validator in legacy_validators: + logger.debug( + f"Applying validator: {validator.identifier} to {identifier}" + ) + resource_dict = validator.validator_function(resource_dict) + logger.debug( + f"Validator {validator.identifier} completed for {identifier}" + ) + + # Validate the migrated resource (don't save yet) + resource = resource_class.model_validate(resource_dict) + migrations.append((identifier, resource)) + + except Exception as e: + logger.error(f"Migration failed for {identifier}: {e}") + console_print( + f"{ERROR}Migration validation failed for {identifier}: {e}", + stderr=True, + ) + console_print( + f"{ERROR}No resources were modified (all-or-nothing transaction safety)", + stderr=True, + ) + raise typer.Exit(1) from e + + # Phase 2: All validations passed, now save all resources + logger.info( + f"All {len(migrations)} resources validated successfully, applying changes..." + ) + + for idx, (identifier, migrated_resource) in enumerate(migrations): + status.update( + ADO_SPINNER_SAVING_TO_DB + f" ({idx + 1}/{len(migrations)})" + ) + + try: + sql_store.updateResource(resource=migrated_resource) + except Exception as e: + logger.error(f"Failed to save {identifier}: {e}") + console_print( + f"{ERROR}Failed to save {identifier}. Database may be in inconsistent state.", + stderr=True, + ) + console_print( + f"{ERROR}Manual intervention may be required to restore consistency.", + stderr=True, + ) + raise typer.Exit(1) from e else: # Normal upgrade path without legacy validators try: From e7bc13495ef6c666c95e0ea4a8f2654a3114d615 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 11:04:35 +0000 Subject: [PATCH 15/55] test: add tests for new functionalities Signed-off-by: Alessandro Pomponio --- tests/core/test_upgrade_transaction_safety.py | 277 +++++++++++++++++ tests/core/test_validator_scope_fixes.py | 283 ++++++++++++++++++ 2 files changed, 560 insertions(+) create mode 100644 tests/core/test_upgrade_transaction_safety.py create mode 100644 tests/core/test_validator_scope_fixes.py diff --git a/tests/core/test_upgrade_transaction_safety.py b/tests/core/test_upgrade_transaction_safety.py new file mode 100644 index 000000000..895a3b526 --- /dev/null +++ b/tests/core/test_upgrade_transaction_safety.py @@ -0,0 +1,277 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Tests for Phase 1 transaction safety in upgrade handler""" + +from unittest.mock import MagicMock, patch + +import pytest +import typer + +from orchestrator.core.legacy.registry import LegacyValidatorRegistry, legacy_validator +from orchestrator.core.resources import CoreResourceKinds + + +class TestUpgradeTransactionSafety: + """Test transaction safety in upgrade handler - validate-all-before-save pattern""" + + def setup_method(self) -> None: + """Clear the registry before each test""" + LegacyValidatorRegistry._validators = {} + + def test_all_resources_validated_before_any_saved(self) -> None: + """Test that all resources are validated before any are saved""" + + # Register a test validator + @legacy_validator( + identifier="test_transaction_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["old_field"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test transaction validator", + ) + def test_validator(data: dict) -> dict: + if "config" in data and "old_field" in data["config"]: + data["config"]["new_field"] = data["config"].pop("old_field") + return data + + # Create mock resources + mock_resource1 = MagicMock() + mock_resource1.model_dump.return_value = { + "kind": "samplestore", + "identifier": "res1", + "config": {"old_field": "value1"}, + } + + mock_resource2 = MagicMock() + mock_resource2.model_dump.return_value = { + "kind": "samplestore", + "identifier": "res2", + "config": {"old_field": "value2"}, + } + + # Mock resource class + mock_resource_class = MagicMock() + validated_resources = [] + + def mock_validate(data: dict) -> MagicMock: + validated = MagicMock() + validated.model_dump.return_value = data + validated_resources.append(data["identifier"]) + return validated + + mock_resource_class.model_validate.side_effect = mock_validate + + # Mock SQL store + mock_sql_store = MagicMock() + mock_sql_store.getResourcesOfKind.return_value = { + "res1": mock_resource1, + "res2": mock_resource2, + } + mock_sql_store.getResourceIdentifiersOfKind.return_value = { + "IDENTIFIER": ["res1", "res2"] + } + mock_sql_store.getResourceRaw.side_effect = lambda id: ( + mock_resource1.model_dump() if id == "res1" else mock_resource2.model_dump() + ) + + update_calls = [] + + def track_update(resource: MagicMock) -> None: + update_calls.append(resource.model_dump()["identifier"]) + + mock_sql_store.updateResource.side_effect = track_update + + # Mock parameters + mock_params = MagicMock() + mock_params.apply_legacy_validator = ["test_transaction_validator"] + mock_params.list_legacy_validators = False + mock_params.ado_configuration.project_context = "test_context" + + # Patch dependencies + with ( + patch( + "orchestrator.cli.utils.resources.handlers.get_sql_store", + return_value=mock_sql_store, + ), + patch( + "orchestrator.core.kindmap", + {"samplestore": mock_resource_class}, + ), + patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), + patch("orchestrator.cli.utils.resources.handlers.Status"), + patch("orchestrator.cli.utils.resources.handlers.console_print"), + ): + from orchestrator.cli.utils.resources.handlers import handle_ado_upgrade + + # Call the upgrade handler + handle_ado_upgrade( + parameters=mock_params, + resource_type=CoreResourceKinds.SAMPLESTORE, + ) + + # Verify: all resources validated before any saved + # Both resources should be validated + assert len(validated_resources) == 2 + assert "res1" in validated_resources + assert "res2" in validated_resources + + # Both resources should be saved + assert len(update_calls) == 2 + assert "res1" in update_calls + assert "res2" in update_calls + + def test_validation_failure_prevents_all_saves(self) -> None: + """Test that if any validation fails, no resources are saved""" + + # Register a test validator + @legacy_validator( + identifier="test_failing_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["old_field"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test failing validator", + ) + def test_validator(data: dict) -> dict: + if "config" in data and "old_field" in data["config"]: + data["config"]["new_field"] = data["config"].pop("old_field") + return data + + # Create mock resources - one valid, one will fail validation + mock_resource1 = MagicMock() + mock_resource1.model_dump.return_value = { + "kind": "samplestore", + "identifier": "res1", + "config": {"old_field": "value1"}, + } + + mock_resource2 = MagicMock() + mock_resource2.model_dump.return_value = { + "kind": "samplestore", + "identifier": "res2", + "config": {"old_field": "value2"}, + } + + # Mock resource class - second validation fails + mock_resource_class = MagicMock() + validation_count = [0] + + def mock_validate(data: dict) -> MagicMock: + validation_count[0] += 1 + if validation_count[0] == 2: + # Second validation fails - raise a simple ValueError + raise ValueError("Validation failed for resource res2") + validated = MagicMock() + validated.model_dump.return_value = data + return validated + + mock_resource_class.model_validate.side_effect = mock_validate + + # Mock SQL store + mock_sql_store = MagicMock() + mock_sql_store.getResourcesOfKind.return_value = { + "res1": mock_resource1, + "res2": mock_resource2, + } + mock_sql_store.getResourceIdentifiersOfKind.return_value = { + "IDENTIFIER": ["res1", "res2"] + } + mock_sql_store.getResourceRaw.side_effect = lambda id: ( + mock_resource1.model_dump() if id == "res1" else mock_resource2.model_dump() + ) + + # Mock parameters + mock_params = MagicMock() + mock_params.apply_legacy_validator = ["test_failing_validator"] + mock_params.list_legacy_validators = False + mock_params.ado_configuration.project_context = "test_context" + + # Patch dependencies + with ( + patch( + "orchestrator.cli.utils.resources.handlers.get_sql_store", + return_value=mock_sql_store, + ), + patch( + "orchestrator.core.kindmap", + {"samplestore": mock_resource_class}, + ), + patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), + patch("orchestrator.cli.utils.resources.handlers.Status"), + patch( + "orchestrator.cli.utils.resources.handlers.console_print" + ) as mock_print, + ): + from orchestrator.cli.utils.resources.handlers import handle_ado_upgrade + + # Should raise typer.Exit due to validation failure + with pytest.raises(typer.Exit) as exc_info: + handle_ado_upgrade( + parameters=mock_params, + resource_type=CoreResourceKinds.SAMPLESTORE, + ) + + assert exc_info.value.exit_code == 1 + + # Verify: NO resources were saved (transaction safety) + mock_sql_store.updateResource.assert_not_called() + + # Verify error was printed + mock_print.assert_called() + + def test_empty_resource_list_handled_gracefully(self) -> None: + """Test that empty resource list is handled without errors""" + + # Register a test validator + @legacy_validator( + identifier="test_empty_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["old_field"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Test empty validator", + ) + def test_validator(data: dict) -> dict: + return data + + # Mock SQL store with no resources + mock_sql_store = MagicMock() + mock_sql_store.getResourcesOfKind.return_value = {} + mock_sql_store.getResourceIdentifiersOfKind.return_value = {"IDENTIFIER": []} + + # Mock parameters + mock_params = MagicMock() + mock_params.apply_legacy_validator = ["test_empty_validator"] + mock_params.list_legacy_validators = False + mock_params.ado_configuration.project_context = "test_context" + + # Patch dependencies + with ( + patch( + "orchestrator.cli.utils.resources.handlers.get_sql_store", + return_value=mock_sql_store, + ), + patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), + patch("orchestrator.cli.utils.resources.handlers.Status"), + patch( + "orchestrator.cli.utils.resources.handlers.console_print" + ) as mock_print, + ): + from orchestrator.cli.utils.resources.handlers import handle_ado_upgrade + + # Should complete without error + handle_ado_upgrade( + parameters=mock_params, + resource_type=CoreResourceKinds.SAMPLESTORE, + ) + + # Verify: no updates attempted + mock_sql_store.updateResource.assert_not_called() + + # Verify message printed + mock_print.assert_called() + + +# Made with Bob diff --git a/tests/core/test_validator_scope_fixes.py b/tests/core/test_validator_scope_fixes.py new file mode 100644 index 000000000..e15beca10 --- /dev/null +++ b/tests/core/test_validator_scope_fixes.py @@ -0,0 +1,283 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Tests for Phase 1 validator scope fixes - verifying validators only operate on config level""" + +from orchestrator.core.legacy.registry import LegacyValidatorRegistry + + +class TestValidatorScopeFixes: + """Test that validators correctly operate only on config level after Phase 1 fixes""" + + @classmethod + def setup_class(cls) -> None: + """Import validators once for all tests in this class""" + # Import all validators to register them + from orchestrator.cli.utils.legacy.common import import_legacy_validators + + import_legacy_validators() + + def test_discoveryspace_properties_removal_scope(self) -> None: + """Verify properties_field_removal only modifies config, not resource level""" + + # Test data with 'properties' at both levels + resource_data = { + "kind": "discoveryspace", + "identifier": "test-space", + "properties": "SHOULD_NOT_BE_REMOVED", # Resource level + "config": { + "properties": ["prop1", "prop2"], # Config level (should be removed) + "sampleStoreIdentifier": "store-1", + }, + } + + # Get validator + validator = LegacyValidatorRegistry.get_validator( + "discoveryspace_properties_field_removal" + ) + assert validator is not None + + # Apply validator + result = validator.validator_function(resource_data.copy()) + + # Verify: resource-level field unchanged, config-level field removed + assert "properties" in result # Resource level preserved + assert result["properties"] == "SHOULD_NOT_BE_REMOVED" + assert "properties" not in result["config"] # Config level removed + assert result["config"]["sampleStoreIdentifier"] == "store-1" + + def test_discoveryspace_entitysource_migration_scope(self) -> None: + """Verify entitysource_to_samplestore only modifies config, not resource level""" + + # Test data with entitySourceIdentifier at both levels + resource_data = { + "kind": "discoveryspace", + "identifier": "test-space", + "entitySourceIdentifier": "SHOULD_NOT_BE_REMOVED", # Resource level + "config": { + "entitySourceIdentifier": "old-source", # Config level (should migrate) + }, + } + + # Get validator + validator = LegacyValidatorRegistry.get_validator( + "discoveryspace_entitysource_to_samplestore" + ) + assert validator is not None + + # Apply validator + result = validator.validator_function(resource_data.copy()) + + # Verify: resource-level field unchanged, config-level field migrated + assert "entitySourceIdentifier" in result # Resource level preserved + assert result["entitySourceIdentifier"] == "SHOULD_NOT_BE_REMOVED" + assert "entitySourceIdentifier" not in result["config"] # Config level removed + assert result["config"]["sampleStoreIdentifier"] == "old-source" # Migrated + + def test_operation_actuators_removal_scope(self) -> None: + """Verify actuators_field_removal only modifies config, not resource level""" + + # Test data with actuators at both levels + resource_data = { + "kind": "operation", + "identifier": "test-op", + "actuators": "SHOULD_NOT_BE_REMOVED", # Resource level + "config": { + "actuators": ["act1", "act2"], # Config level (should be removed) + "operatorIdentifier": "op1", + }, + } + + # Get validator + validator = LegacyValidatorRegistry.get_validator( + "operation_actuators_field_removal" + ) + assert validator is not None + + # Apply validator + result = validator.validator_function(resource_data.copy()) + + # Verify: resource-level field unchanged, config-level field removed + assert "actuators" in result # Resource level preserved + assert result["actuators"] == "SHOULD_NOT_BE_REMOVED" + assert "actuators" not in result["config"] # Config level removed + assert result["config"]["operatorIdentifier"] == "op1" + + def test_samplestore_module_type_entitysource_migration_scope(self) -> None: + """Verify entitysource module type migration only modifies config, not resource level""" + + # Test data with moduleType at both levels + resource_data = { + "kind": "samplestore", + "type": "csv", + "identifier": "test-store", + "moduleType": "SHOULD_NOT_BE_REMOVED", # Resource level + "config": { + "moduleType": "entity_source", # Config level (should migrate) + }, + } + + # Get validator for module type entitysource migration + validator = LegacyValidatorRegistry.get_validator( + "samplestore_module_type_entitysource_to_samplestore" + ) + assert validator is not None + + # Apply validator + result = validator.validator_function(resource_data.copy()) + + # Verify: resource-level field unchanged, config-level field migrated + assert "moduleType" in result # Resource level preserved + assert result["moduleType"] == "SHOULD_NOT_BE_REMOVED" + assert result["config"]["moduleType"] == "sample_store" # Migrated + + def test_samplestore_csv_migration_scope(self) -> None: + """Verify CSV v1 to v2 migration only modifies config, not resource level""" + + # Test data with constitutivePropertyColumns at both levels + resource_data = { + "kind": "samplestore", + "type": "csv", + "identifier": "test-store", + "constitutivePropertyColumns": "SHOULD_NOT_BE_REMOVED", # Resource level + "config": { + "identifierColumn": "id", + "constitutivePropertyColumns": ["prop1", "prop2"], # Config (migrate) + "experiments": [ + { + "experimentIdentifier": "exp1", + "actuatorIdentifier": "act1", + "propertyMap": ["obs1", "obs2"], + } + ], + }, + } + + # Get validator + validator = LegacyValidatorRegistry.get_validator( + "csv_constitutive_columns_migration" + ) + assert validator is not None + + # Apply validator + result = validator.validator_function(resource_data.copy()) + + # Verify: resource-level field unchanged, config-level field migrated + assert "constitutivePropertyColumns" in result # Resource level preserved + assert result["constitutivePropertyColumns"] == "SHOULD_NOT_BE_REMOVED" + assert "constitutivePropertyColumns" not in result["config"] # Config removed + # Verify migration happened in config + exp = result["config"]["experiments"][0] + assert "propertyMap" not in exp + assert "observedPropertyMap" in exp + assert exp["observedPropertyMap"] == ["obs1", "obs2"] + assert "constitutivePropertyMap" in exp + assert exp["constitutivePropertyMap"] == ["prop1", "prop2"] + + def test_resource_kind_field_operates_at_resource_level(self) -> None: + """Verify resource-level validators (like kind migration) operate at resource level""" + + # Test data with entitysource kind at resource level + resource_data = { + "kind": "entitysource", # Resource level (should be migrated) + "type": "csv", + "identifier": "test-store", + "config": { + "identifierColumn": "id", + }, + } + + # Get validator + validator = LegacyValidatorRegistry.get_validator( + "samplestore_kind_entitysource_to_samplestore" + ) + assert validator is not None + + # Apply validator + result = validator.validator_function(resource_data.copy()) + + # Verify: resource-level kind field was migrated + assert result["kind"] == "samplestore" + assert result["type"] == "csv" + assert result["identifier"] == "test-store" + + def test_validators_preserve_unrelated_fields(self) -> None: + """Verify validators don't modify unrelated fields at any level""" + + # Test data with many fields + resource_data = { + "kind": "discoveryspace", + "identifier": "test-space", + "unrelated_resource_field": "preserve_me", + "config": { + "properties": ["prop1", "prop2"], # Will be removed + "sampleStoreIdentifier": "store-1", + "unrelated_config_field": "preserve_me_too", + "nested": { + "deep_field": "also_preserve", + }, + }, + } + + # Get validator + validator = LegacyValidatorRegistry.get_validator( + "discoveryspace_properties_field_removal" + ) + assert validator is not None + + # Apply validator + result = validator.validator_function(resource_data.copy()) + + # Verify: unrelated fields preserved at all levels + assert result["unrelated_resource_field"] == "preserve_me" + assert result["config"]["unrelated_config_field"] == "preserve_me_too" + assert result["config"]["nested"]["deep_field"] == "also_preserve" + assert result["config"]["sampleStoreIdentifier"] == "store-1" + # But deprecated field removed + assert "properties" not in result["config"] + + def test_validators_handle_missing_config_gracefully(self) -> None: + """Verify validators handle missing config field gracefully""" + + # Test data without config + resource_data = { + "kind": "discoveryspace", + "identifier": "test-space", + } + + # Get validator + validator = LegacyValidatorRegistry.get_validator( + "discoveryspace_properties_field_removal" + ) + assert validator is not None + + # Apply validator - should not crash + result = validator.validator_function(resource_data.copy()) + + # Verify: data unchanged + assert result == resource_data + + def test_validators_handle_empty_config_gracefully(self) -> None: + """Verify validators handle empty config dict gracefully""" + + # Test data with empty config + resource_data = { + "kind": "discoveryspace", + "identifier": "test-space", + "config": {}, + } + + # Get validator + validator = LegacyValidatorRegistry.get_validator( + "discoveryspace_properties_field_removal" + ) + assert validator is not None + + # Apply validator - should not crash + result = validator.validator_function(resource_data.copy()) + + # Verify: data unchanged + assert result == resource_data + + +# Made with Bob From 68cd9df8766c9b87fcfaceb479fc65b9436f7618 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 11:14:15 +0000 Subject: [PATCH 16/55] refactor(core): simplify importing of legacy validators Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 6 ++---- orchestrator/cli/utils/legacy/common.py | 20 ------------------- orchestrator/cli/utils/legacy/list.py | 5 ++--- orchestrator/cli/utils/resources/handlers.py | 11 ++++------ .../core/legacy/validators/__init__.py | 10 ++++++++++ .../validators/discoveryspace/__init__.py | 7 +++++++ .../legacy/validators/operation/__init__.py | 7 +++++++ .../legacy/validators/resource/__init__.py | 4 ++++ .../legacy/validators/samplestore/__init__.py | 7 +++++++ 9 files changed, 43 insertions(+), 34 deletions(-) diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index e83069bc1..bba5759b7 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -82,16 +82,14 @@ def handle_validation_error_with_legacy_suggestions( Raises: typer.Exit: Always exits with code 1 """ + # Import validators package to trigger registration via __init__.py + import orchestrator.core.legacy.validators # noqa: F401 from orchestrator.cli.utils.legacy.common import ( extract_deprecated_fields_from_validation_error, - import_legacy_validators, print_validator_suggestions, ) from orchestrator.core.legacy.registry import LegacyValidatorRegistry - # Import validators to ensure they're registered - import_legacy_validators() - # Extract field paths, error details, and leaf field names from validation error full_field_paths, field_errors, leaf_field_names = ( extract_deprecated_fields_from_validation_error(error) diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index 8d338273a..bcc835f3c 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -13,26 +13,6 @@ from orchestrator.core.resources import CoreResourceKinds -def import_legacy_validators() -> None: - """Import all legacy validator modules to ensure they're registered""" - # Import validator modules to trigger decorator registration - try: - # Discovery Space validators - import orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore # noqa: F401 - import orchestrator.core.legacy.validators.discoveryspace.properties_field_removal # noqa: F401 - - # Operation validators - import orchestrator.core.legacy.validators.operation.actuators_field_removal # noqa: F401 - import orchestrator.core.legacy.validators.operation.randomwalk_mode_to_sampler_config # noqa: F401 - - # Sample Store validators - import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore # noqa: F401 - import orchestrator.core.legacy.validators.samplestore.entitysource_migrations # noqa: F401 - import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration # noqa: F401 - except ImportError: - pass # Validators may not be available in all installations - - def print_validator_suggestions( validators: list["LegacyValidatorMetadata"], resource_type: "CoreResourceKinds", diff --git a/orchestrator/cli/utils/legacy/list.py b/orchestrator/cli/utils/legacy/list.py index 1a7a84f5a..f0a0205c3 100644 --- a/orchestrator/cli/utils/legacy/list.py +++ b/orchestrator/cli/utils/legacy/list.py @@ -6,7 +6,6 @@ from rich.console import Console from rich.panel import Panel -from orchestrator.cli.utils.legacy.common import import_legacy_validators from orchestrator.core.legacy.registry import LegacyValidatorRegistry from orchestrator.core.resources import CoreResourceKinds @@ -19,8 +18,8 @@ def list_legacy_validators(resource_type: CoreResourceKinds) -> None: """ console = Console() - # Import all validator modules to ensure they're registered - import_legacy_validators() + # Import validators package to trigger registration via __init__.py + import orchestrator.core.legacy.validators # noqa: F401 # Get validators for this resource type validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index a3b3641c5..0c7895e62 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -242,10 +242,8 @@ def handle_ado_upgrade( parameters: Command parameters including legacy validator options resource_type: The type of resource to upgrade """ - from orchestrator.cli.utils.legacy.common import import_legacy_validators - - # Import all validator modules to ensure they're registered - import_legacy_validators() + # Import validators package to trigger registration via __init__.py + import orchestrator.core.legacy.validators # noqa: F401 # Handle --list-legacy-validators flag if parameters.list_legacy_validators: @@ -393,15 +391,14 @@ def _handle_upgrade_validation_error( from orchestrator.cli.utils.legacy.common import ( extract_deprecated_fields_from_value_error, - import_legacy_validators, print_validator_suggestions, ) from orchestrator.core.legacy.registry import LegacyValidatorRegistry console = Console() - # Import all validator modules to ensure they're registered - import_legacy_validators() + # Import validators package to trigger registration via __init__.py + import orchestrator.core.legacy.validators # noqa: F401 # Extract field paths, error details, and leaf field names from the error full_field_paths, field_errors, leaf_field_names = ( diff --git a/orchestrator/core/legacy/validators/__init__.py b/orchestrator/core/legacy/validators/__init__.py index e185d705e..5a0c64aa2 100644 --- a/orchestrator/core/legacy/validators/__init__.py +++ b/orchestrator/core/legacy/validators/__init__.py @@ -3,4 +3,14 @@ """Legacy validators for deprecated resource formats""" +# Import all validator subpackages to trigger registration +from orchestrator.core.legacy.validators import ( + discoveryspace, + operation, + resource, + samplestore, +) + +__all__ = ["discoveryspace", "operation", "resource", "samplestore"] + # Made with Bob diff --git a/orchestrator/core/legacy/validators/discoveryspace/__init__.py b/orchestrator/core/legacy/validators/discoveryspace/__init__.py index 7475ec987..5ca04fb56 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/__init__.py +++ b/orchestrator/core/legacy/validators/discoveryspace/__init__.py @@ -3,4 +3,11 @@ """Legacy validators for discovery space migrations""" +from orchestrator.core.legacy.validators.discoveryspace import ( + entitysource_to_samplestore, + properties_field_removal, +) + +__all__ = ["entitysource_to_samplestore", "properties_field_removal"] + # Made with Bob diff --git a/orchestrator/core/legacy/validators/operation/__init__.py b/orchestrator/core/legacy/validators/operation/__init__.py index 6ccf17a45..a62c44a1f 100644 --- a/orchestrator/core/legacy/validators/operation/__init__.py +++ b/orchestrator/core/legacy/validators/operation/__init__.py @@ -3,4 +3,11 @@ """Legacy validators for operation migrations""" +from orchestrator.core.legacy.validators.operation import ( + actuators_field_removal, + randomwalk_mode_to_sampler_config, +) + +__all__ = ["actuators_field_removal", "randomwalk_mode_to_sampler_config"] + # Made with Bob diff --git a/orchestrator/core/legacy/validators/resource/__init__.py b/orchestrator/core/legacy/validators/resource/__init__.py index 7847cc0ed..171b18dfc 100644 --- a/orchestrator/core/legacy/validators/resource/__init__.py +++ b/orchestrator/core/legacy/validators/resource/__init__.py @@ -3,4 +3,8 @@ """Legacy validators for generic resource migrations""" +from orchestrator.core.legacy.validators.resource import entitysource_to_samplestore + +__all__ = ["entitysource_to_samplestore"] + # Made with Bob diff --git a/orchestrator/core/legacy/validators/samplestore/__init__.py b/orchestrator/core/legacy/validators/samplestore/__init__.py index 439a403d4..4dec63489 100644 --- a/orchestrator/core/legacy/validators/samplestore/__init__.py +++ b/orchestrator/core/legacy/validators/samplestore/__init__.py @@ -3,4 +3,11 @@ """Legacy validators for sample store migrations""" +from orchestrator.core.legacy.validators.samplestore import ( + entitysource_migrations, + v1_to_v2_csv_migration, +) + +__all__ = ["entitysource_migrations", "v1_to_v2_csv_migration"] + # Made with Bob From f5599d0943a304231f0bc29d3cb3c81d6601d021 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 11:19:43 +0000 Subject: [PATCH 17/55] feat(core): add utility getters/setters for dictionaries Signed-off-by: Alessandro Pomponio --- orchestrator/core/legacy/utils.py | 102 ++++++++++++++++ tests/core/test_legacy_utils.py | 192 ++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 orchestrator/core/legacy/utils.py create mode 100644 tests/core/test_legacy_utils.py diff --git a/orchestrator/core/legacy/utils.py b/orchestrator/core/legacy/utils.py new file mode 100644 index 000000000..db1695c96 --- /dev/null +++ b/orchestrator/core/legacy/utils.py @@ -0,0 +1,102 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Utility functions for legacy validators""" + + +def get_nested_value(data: dict, path: str) -> tuple[dict | None, str | None]: + """Navigate to a nested field path and return parent dict and field name + + Args: + data: The data dictionary + path: Dot-separated path (e.g., "config.specification.module.moduleType") + + Returns: + Tuple of (parent_dict, field_name) or (None, None) if path doesn't exist + + Example: + parent, field = get_nested_value(data, "config.properties") + if parent and field: + parent.pop(field, None) + """ + parts = path.split(".") + current = data + + # Navigate to parent + for part in parts[:-1]: + if not isinstance(current, dict) or part not in current: + return None, None + current = current[part] + + # Return parent dict and final field name + if isinstance(current, dict): + return current, parts[-1] + + return None, None + + +def set_nested_value(data: dict, path: str, value: object) -> bool: + """Set a value at a nested field path + + Args: + data: The data dictionary + path: Dot-separated path + value: Value to set + + Returns: + True if successful, False if path doesn't exist + + Example: + data = {"config": {"specification": {"module": {}}}} + set_nested_value(data, "config.specification.module.type", "sample_store") + # data is now {"config": {"specification": {"module": {"type": "sample_store"}}}} + """ + parent, field = get_nested_value(data, path) + if parent is not None and field is not None: + parent[field] = value + return True + return False + + +def remove_nested_field(data: dict, path: str) -> bool: + """Remove a field at a nested path + + Args: + data: The data dictionary + path: Dot-separated path + + Returns: + True if field was removed, False if path doesn't exist + + Example: + data = {"config": {"properties": ["a", "b"], "other": "value"}} + remove_nested_field(data, "config.properties") + # data is now {"config": {"other": "value"}} + """ + parent, field = get_nested_value(data, path) + if parent is not None and field is not None and field in parent: + parent.pop(field) + return True + return False + + +def has_nested_field(data: dict, path: str) -> bool: + """Check if a nested field path exists + + Args: + data: The data dictionary + path: Dot-separated path + + Returns: + True if the field exists, False otherwise + + Example: + data = {"config": {"specification": {"module": {"moduleType": "test"}}}} + has_nested_field(data, "config.specification.module.moduleType") # Returns True + has_nested_field(data, "config.nonexistent") # Returns False + """ + parent, field = get_nested_value(data, path) + return parent is not None and field is not None and field in parent + + +# Made with Bob diff --git a/tests/core/test_legacy_utils.py b/tests/core/test_legacy_utils.py new file mode 100644 index 000000000..69bd21b6f --- /dev/null +++ b/tests/core/test_legacy_utils.py @@ -0,0 +1,192 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Tests for legacy validator utility functions""" + +from orchestrator.core.legacy.utils import ( + get_nested_value, + has_nested_field, + remove_nested_field, + set_nested_value, +) + + +class TestGetNestedValue: + """Tests for get_nested_value function""" + + def test_simple_path(self) -> None: + """Test getting a simple top-level field""" + data = {"config": {"properties": ["a", "b"]}} + parent, field = get_nested_value(data, "config") + assert parent == data + assert field == "config" + + def test_nested_path(self) -> None: + """Test getting a nested field""" + data = {"config": {"specification": {"module": {"moduleType": "test"}}}} + parent, field = get_nested_value(data, "config.specification.module.moduleType") + assert parent == {"moduleType": "test"} + assert field == "moduleType" + + def test_nonexistent_path(self) -> None: + """Test getting a path that doesn't exist""" + data = {"config": {}} + parent, field = get_nested_value(data, "config.nonexistent.field") + assert parent is None + assert field is None + + def test_path_through_non_dict(self) -> None: + """Test path that goes through a non-dict value""" + data = {"config": "string_value"} + parent, field = get_nested_value(data, "config.field") + assert parent is None + assert field is None + + +class TestSetNestedValue: + """Tests for set_nested_value function""" + + def test_set_simple_value(self) -> None: + """Test setting a simple nested value""" + data = {"config": {}} + result = set_nested_value(data, "config.test", "value") + assert result is True + assert data["config"]["test"] == "value" + + def test_set_deeply_nested_value(self) -> None: + """Test setting a deeply nested value""" + data = {"config": {"specification": {"module": {}}}} + result = set_nested_value(data, "config.specification.module.type", "new_type") + assert result is True + assert data["config"]["specification"]["module"]["type"] == "new_type" + + def test_set_nonexistent_path(self) -> None: + """Test setting a value on a nonexistent path""" + data = {"config": {}} + result = set_nested_value(data, "config.nonexistent.field", "value") + assert result is False + assert "nonexistent" not in data["config"] + + def test_overwrite_existing_value(self) -> None: + """Test overwriting an existing value""" + data = {"config": {"test": "old_value"}} + result = set_nested_value(data, "config.test", "new_value") + assert result is True + assert data["config"]["test"] == "new_value" + + +class TestRemoveNestedField: + """Tests for remove_nested_field function""" + + def test_remove_simple_field(self) -> None: + """Test removing a simple field""" + data = {"config": {"properties": ["a", "b"], "other": "value"}} + result = remove_nested_field(data, "config.properties") + assert result is True + assert "properties" not in data["config"] + assert data["config"]["other"] == "value" + + def test_remove_deeply_nested_field(self) -> None: + """Test removing a deeply nested field""" + data = { + "config": { + "specification": {"module": {"moduleType": "old", "other": "value"}} + } + } + result = remove_nested_field(data, "config.specification.module.moduleType") + assert result is True + assert "moduleType" not in data["config"]["specification"]["module"] + assert data["config"]["specification"]["module"]["other"] == "value" + + def test_remove_nonexistent_field(self) -> None: + """Test removing a field that doesn't exist""" + data = {"config": {}} + result = remove_nested_field(data, "config.nonexistent") + assert result is False + + def test_remove_field_idempotent(self) -> None: + """Test that removing a field twice is safe""" + data = {"config": {"test": "value"}} + result1 = remove_nested_field(data, "config.test") + assert result1 is True + result2 = remove_nested_field(data, "config.test") + assert result2 is False + + +class TestHasNestedField: + """Tests for has_nested_field function""" + + def test_has_simple_field(self) -> None: + """Test checking for a simple field""" + data = {"config": {"properties": ["a", "b"]}} + assert has_nested_field(data, "config.properties") is True + + def test_has_deeply_nested_field(self) -> None: + """Test checking for a deeply nested field""" + data = {"config": {"specification": {"module": {"moduleType": "test"}}}} + assert has_nested_field(data, "config.specification.module.moduleType") is True + + def test_has_nonexistent_field(self) -> None: + """Test checking for a field that doesn't exist""" + data = {"config": {}} + assert has_nested_field(data, "config.nonexistent") is False + + def test_has_field_through_non_dict(self) -> None: + """Test checking for a field through a non-dict value""" + data = {"config": "string_value"} + assert has_nested_field(data, "config.field") is False + + +class TestIntegration: + """Integration tests combining multiple utility functions""" + + def test_check_set_remove_workflow(self) -> None: + """Test a complete workflow: check, set, remove""" + data = {"config": {}} + + # Check field doesn't exist + assert has_nested_field(data, "config.test") is False + + # Set the field + assert set_nested_value(data, "config.test", "value") is True + assert has_nested_field(data, "config.test") is True + assert data["config"]["test"] == "value" + + # Remove the field + assert remove_nested_field(data, "config.test") is True + assert has_nested_field(data, "config.test") is False + + def test_complex_nested_structure(self) -> None: + """Test with a complex nested structure""" + data = { + "metadata": {"name": "test"}, + "config": { + "specification": { + "module": {"moduleType": "entity_source", "moduleName": "test"} + } + }, + } + + # Check existing field + assert has_nested_field(data, "config.specification.module.moduleType") is True + + # Modify the field + assert ( + set_nested_value( + data, "config.specification.module.moduleType", "sample_store" + ) + is True + ) + assert data["config"]["specification"]["module"]["moduleType"] == "sample_store" + + # Remove another field + assert ( + remove_nested_field(data, "config.specification.module.moduleName") is True + ) + assert "moduleName" not in data["config"]["specification"]["module"] + + # Original structure still intact + assert data["metadata"]["name"] == "test" + + +# Made with Bob From 3b8121690684e1d9a3b3254d2bc9ebf8ac16e80e Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 11:20:32 +0000 Subject: [PATCH 18/55] feat(core): add field_paths to validator metadata For safer updates Signed-off-by: Alessandro Pomponio --- orchestrator/core/legacy/metadata.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/orchestrator/core/legacy/metadata.py b/orchestrator/core/legacy/metadata.py index 92ba0e3a0..b5ba7f792 100644 --- a/orchestrator/core/legacy/metadata.py +++ b/orchestrator/core/legacy/metadata.py @@ -56,6 +56,14 @@ class LegacyValidatorMetadata(pydantic.BaseModel): ), ] + field_paths: Annotated[ + list[str], + pydantic.Field( + default_factory=list, + description="Explicit paths to fields (e.g., 'config.properties', 'config.specification.moduleType')", + ), + ] + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) From be8a4ce706dd2210f4ce05755559d5181f741852 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 11:41:12 +0000 Subject: [PATCH 19/55] refactor(legacy): simplify validators Signed-off-by: Alessandro Pomponio --- orchestrator/core/legacy/registry.py | 4 + .../entitysource_to_samplestore.py | 25 +++-- .../properties_field_removal.py | 11 +- .../operation/actuators_field_removal.py | 15 +-- .../randomwalk_mode_to_sampler_config.py | 50 ++++++--- .../resource/entitysource_to_samplestore.py | 6 +- .../samplestore/entitysource_migrations.py | 104 ++++++------------ .../samplestore/v1_to_v2_csv_migration.py | 32 +++--- 8 files changed, 117 insertions(+), 130 deletions(-) diff --git a/orchestrator/core/legacy/registry.py b/orchestrator/core/legacy/registry.py index 8c830972a..61f052cba 100644 --- a/orchestrator/core/legacy/registry.py +++ b/orchestrator/core/legacy/registry.py @@ -87,6 +87,7 @@ def legacy_validator( deprecated_from_version: str, removed_from_version: str, description: str, + field_paths: list[str] | None = None, ) -> Callable[[Callable[[dict], dict]], Callable[[dict], dict]]: """Decorator to register a legacy validator function @@ -97,6 +98,7 @@ def legacy_validator( deprecated_from_version: ADO version when these fields were deprecated removed_from_version: ADO version when automatic upgrade was removed description: Human-readable description of what this validator does + field_paths: Optional explicit paths to fields (e.g., 'config.properties') Returns: Decorator function that registers the validator @@ -106,6 +108,7 @@ def legacy_validator( identifier="csv_constitutive_columns_migration", resource_type=CoreResourceKinds.SAMPLESTORE, deprecated_fields=["constitutivePropertyColumns", "propertyMap"], + field_paths=["config.specification.constitutivePropertyColumns"], deprecated_from_version="1.3.5", removed_from_version="1.6.0", description="Migrates CSV sample stores from v1 to v2 format" @@ -124,6 +127,7 @@ def decorator(func: Callable[[dict], dict]) -> Callable[[dict], dict]: removed_from_version=removed_from_version, description=description, validator_function=func, + field_paths=field_paths or [], ) LegacyValidatorRegistry.register(metadata) diff --git a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py index 0a53f260b..c1d48db5a 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py @@ -4,6 +4,11 @@ """Legacy validator for renaming entitySourceIdentifier to sampleStoreIdentifier""" from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.legacy.utils import ( + get_nested_value, + remove_nested_field, + set_nested_value, +) from orchestrator.core.resources import CoreResourceKinds @@ -14,6 +19,7 @@ deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Renames 'entitySourceIdentifier' to 'sampleStoreIdentifier' in discovery space configurations", + field_paths=["config.entitySourceIdentifier"], ) def rename_entitysource_identifier(data: dict) -> dict: """Rename entitySourceIdentifier to sampleStoreIdentifier @@ -40,16 +46,17 @@ def rename_entitysource_identifier(data: dict) -> dict: if not isinstance(data, dict): return data - old_key = "entitySourceIdentifier" - new_key = "sampleStoreIdentifier" + old_path = "config.entitySourceIdentifier" + new_path = "config.sampleStoreIdentifier" - # Only check in config (where the field actually exists) - if ( - "config" in data - and isinstance(data["config"], dict) - and old_key in data["config"] - ): - data["config"][new_key] = data["config"].pop(old_key) + # Get the old value if it exists + parent, field_name = get_nested_value(data, old_path) + if parent is not None and field_name in parent: + old_value = parent[field_name] + # Set the new value + set_nested_value(data, new_path, old_value) + # Remove the old field + remove_nested_field(data, old_path) return data diff --git a/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py index 0c7308d18..54a06ee30 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py +++ b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py @@ -4,6 +4,7 @@ """Legacy validator for removing deprecated properties field from discovery spaces""" from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.legacy.utils import remove_nested_field from orchestrator.core.resources import CoreResourceKinds @@ -11,6 +12,7 @@ identifier="discoveryspace_properties_field_removal", resource_type=CoreResourceKinds.DISCOVERYSPACE, deprecated_fields=["properties"], + field_paths=["config.properties"], deprecated_from_version="0.10.1", removed_from_version="1.0.0", description="Removes the deprecated 'properties' field from discovery space configurations", @@ -36,13 +38,8 @@ def remove_properties_field(data: dict) -> dict: Returns: The migrated resource data dictionary """ - - if not isinstance(data, dict): - return data - - # Only check in config (where the field actually exists) - if "config" in data and isinstance(data["config"], dict): - data["config"].pop("properties", None) + if isinstance(data, dict): + remove_nested_field(data, "config.properties") return data diff --git a/orchestrator/core/legacy/validators/operation/actuators_field_removal.py b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py index bafc618ca..2ec227054 100644 --- a/orchestrator/core/legacy/validators/operation/actuators_field_removal.py +++ b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py @@ -4,6 +4,7 @@ """Legacy validator for removing deprecated actuators field from operations""" from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.legacy.utils import remove_nested_field from orchestrator.core.resources import CoreResourceKinds @@ -11,6 +12,7 @@ identifier="operation_actuators_field_removal", resource_type=CoreResourceKinds.OPERATION, deprecated_fields=["actuators"], + field_paths=["config.actuators"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Removes the deprecated 'actuators' field from operation configurations. See https://ibm.github.io/ado/resources/operation/#the-operation-configuration-yaml", @@ -36,17 +38,8 @@ def remove_actuators_field(data: dict) -> dict: Returns: The migrated resource data dictionary """ - - if not isinstance(data, dict): - return data - - # Only check in config (where the field actually exists) - if ( - "config" in data - and isinstance(data["config"], dict) - and "actuators" in data["config"] - ): - data["config"].pop("actuators", None) + if isinstance(data, dict): + remove_nested_field(data, "config.actuators") return data diff --git a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py index e815f194b..92279747b 100644 --- a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py +++ b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py @@ -4,6 +4,12 @@ """Legacy validator for migrating random_walk parameters to samplerConfig""" from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.legacy.utils import ( + get_nested_value, + has_nested_field, + remove_nested_field, + set_nested_value, +) from orchestrator.core.resources import CoreResourceKinds @@ -14,6 +20,11 @@ deprecated_from_version="1.0.1", removed_from_version="1.2", description="Migrates random_walk parameters from flat structure to nested 'samplerConfig'. See https://ibm.github.io/ado/operators/random-walk/#configuring-a-randomwalk", + field_paths=[ + "config.parameters.mode", + "config.parameters.grouping", + "config.parameters.samplerType", + ], ) def migrate_randomwalk_to_sampler_config(data: dict) -> dict: """Migrate random_walk parameters to samplerConfig structure @@ -34,23 +45,32 @@ def migrate_randomwalk_to_sampler_config(data: dict) -> dict: if not isinstance(data, dict): return data - # Check if this is an operation with parameters that need migration - config = data.get("config") - if not isinstance(config, dict): - return data - - parameters = config.get("parameters") - if not isinstance(parameters, dict): - return data - # Check if mode field exists (indicator of old format) - if "mode" not in parameters: + if not has_nested_field(data, "config.parameters.mode"): return data - # Extract the old fields - mode = parameters.pop("mode", None) - grouping = parameters.pop("grouping", None) - sampler_type = parameters.pop("samplerType", None) + # Extract the old fields - has_nested_field already confirmed they exist + mode = None + grouping = None + sampler_type = None + + if has_nested_field(data, "config.parameters.mode"): + parent, field = get_nested_value(data, "config.parameters.mode") + if parent is not None: + mode = parent[field] + remove_nested_field(data, "config.parameters.mode") + + if has_nested_field(data, "config.parameters.grouping"): + parent, field = get_nested_value(data, "config.parameters.grouping") + if parent is not None: + grouping = parent[field] + remove_nested_field(data, "config.parameters.grouping") + + if has_nested_field(data, "config.parameters.samplerType"): + parent, field = get_nested_value(data, "config.parameters.samplerType") + if parent is not None: + sampler_type = parent[field] + remove_nested_field(data, "config.parameters.samplerType") # Create samplerConfig if any of the fields were present if mode is not None or grouping is not None or sampler_type is not None: @@ -62,7 +82,7 @@ def migrate_randomwalk_to_sampler_config(data: dict) -> dict: if sampler_type is not None: sampler_config["samplerType"] = sampler_type - parameters["samplerConfig"] = sampler_config + set_nested_value(data, "config.parameters.samplerConfig", sampler_config) return data diff --git a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py index ac21e7bac..5e7f1dceb 100644 --- a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py @@ -4,6 +4,7 @@ """Legacy validator for migrating entitysource kind to samplestore kind""" from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.legacy.utils import has_nested_field, set_nested_value from orchestrator.core.resources import CoreResourceKinds @@ -14,6 +15,7 @@ deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts resource kind from 'entitysource' to 'samplestore'", + field_paths=["kind"], ) def migrate_entitysource_kind_to_samplestore(data: dict) -> dict: """Migrate old entitysource kind to samplestore @@ -35,8 +37,8 @@ def migrate_entitysource_kind_to_samplestore(data: dict) -> dict: return data # Check if this is an entitysource that needs migration - if data.get("kind") == "entitysource": - data["kind"] = "samplestore" + if has_nested_field(data, "kind") and data.get("kind") == "entitysource": + set_nested_value(data, "kind", "samplestore") return data diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index f94b58f42..c5f6dfcda 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -4,6 +4,11 @@ """Legacy validators for migrating entitysource to samplestore naming""" from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.legacy.utils import ( + get_nested_value, + has_nested_field, + set_nested_value, +) from orchestrator.core.resources import CoreResourceKinds @@ -14,13 +19,13 @@ deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleType value from 'entity_source' to 'sample_store'", + field_paths=["config.moduleType"], ) def migrate_module_type(data: dict) -> dict: """Convert moduleType from entity_source to sample_store - This validator recursively searches for moduleType fields within the config - and converts them from 'entity_source' to 'sample_store'. It operates only - within the config level, matching the original pydantic validator behavior. + This validator checks for moduleType field within the config + and converts it from 'entity_source' to 'sample_store'. Old format: config: @@ -40,26 +45,12 @@ def migrate_module_type(data: dict) -> dict: if not isinstance(data, dict): return data - # Only operate within config - if "config" not in data or not isinstance(data["config"], dict): - return data + # Check and update config.moduleType + if has_nested_field(data, "config.moduleType"): + parent, field = get_nested_value(data, "config.moduleType") + if parent is not None and parent[field] == "entity_source": + set_nested_value(data, "config.moduleType", "sample_store") - def convert_module_type_in_dict(d: dict) -> None: - """Recursively convert moduleType in nested structures""" - if "moduleType" in d and d["moduleType"] == "entity_source": - d["moduleType"] = "sample_store" - - # Check in nested structures - for value in d.values(): - if isinstance(value, dict): - convert_module_type_in_dict(value) - elif isinstance(value, list): - for item in value: - if isinstance(item, dict): - convert_module_type_in_dict(item) - - # Start recursion from config level - convert_module_type_in_dict(data["config"]) return data @@ -70,13 +61,13 @@ def convert_module_type_in_dict(d: dict) -> None: deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleClass values from EntitySource to SampleStore naming (CSVEntitySource -> CSVSampleStore, SQLEntitySource -> SQLSampleStore)", + field_paths=["config.moduleClass"], ) def migrate_module_class(data: dict) -> dict: """Convert moduleClass from EntitySource to SampleStore naming - This validator recursively searches for moduleClass fields within the config - and converts them from EntitySource to SampleStore naming. It operates only - within the config level, matching the original pydantic validator behavior. + This validator checks for moduleClass field within the config + and converts it from EntitySource to SampleStore naming. Old format: config: @@ -96,31 +87,17 @@ def migrate_module_class(data: dict) -> dict: if not isinstance(data, dict): return data - # Only operate within config - if "config" not in data or not isinstance(data["config"], dict): - return data - value_mappings = { "CSVEntitySource": "CSVSampleStore", "SQLEntitySource": "SQLSampleStore", } - def convert_module_class_in_dict(d: dict) -> None: - """Recursively convert moduleClass in nested structures""" - if "moduleClass" in d and d["moduleClass"] in value_mappings: - d["moduleClass"] = value_mappings[d["moduleClass"]] - - # Check in nested structures - for value in d.values(): - if isinstance(value, dict): - convert_module_class_in_dict(value) - elif isinstance(value, list): - for item in value: - if isinstance(item, dict): - convert_module_class_in_dict(item) - - # Start recursion from config level - convert_module_class_in_dict(data["config"]) + # Check and update config.moduleClass + if has_nested_field(data, "config.moduleClass"): + parent, field = get_nested_value(data, "config.moduleClass") + if parent is not None and parent[field] in value_mappings: + set_nested_value(data, "config.moduleClass", value_mappings[parent[field]]) + return data @@ -131,13 +108,13 @@ def convert_module_class_in_dict(d: dict) -> None: deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Updates module paths from entitysource to samplestore (orchestrator.core.entitysource -> orchestrator.core.samplestore)", + field_paths=["config.moduleName"], ) def migrate_module_name(data: dict) -> dict: """Convert moduleName paths from entitysource to samplestore - This validator recursively searches for moduleName fields within the config - and converts paths from entitysource to samplestore. It operates only - within the config level, matching the original pydantic validator behavior. + This validator checks for moduleName field within the config + and converts paths from entitysource to samplestore. Old format: config: @@ -159,34 +136,21 @@ def migrate_module_name(data: dict) -> dict: if not isinstance(data, dict): return data - # Only operate within config - if "config" not in data or not isinstance(data["config"], dict): - return data - path_mappings = { "orchestrator.core.entitysource": "orchestrator.core.samplestore", "orchestrator.plugins.entitysources": "orchestrator.plugins.samplestores", } - def convert_module_name_in_dict(d: dict) -> None: - """Recursively convert moduleName in nested structures""" - if "moduleName" in d and isinstance(d["moduleName"], str): + # Check and update config.moduleName + if has_nested_field(data, "config.moduleName"): + parent, field = get_nested_value(data, "config.moduleName") + if parent is not None and isinstance(parent[field], str): + module_name = parent[field] for old_path, new_path in path_mappings.items(): - if old_path in d["moduleName"]: - d["moduleName"] = d["moduleName"].replace(old_path, new_path) - break - - # Check in nested structures - for value in d.values(): - if isinstance(value, dict): - convert_module_name_in_dict(value) - elif isinstance(value, list): - for item in value: - if isinstance(item, dict): - convert_module_name_in_dict(item) - - # Start recursion from config level - convert_module_name_in_dict(data["config"]) + if old_path in module_name: + module_name = module_name.replace(old_path, new_path) + set_nested_value(data, "config.moduleName", module_name) + return data diff --git a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py index a51b6f681..3acab2d04 100644 --- a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py +++ b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py @@ -4,6 +4,7 @@ """Legacy validator for migrating CSV sample stores from v1 to v2 format""" from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.legacy.utils import has_nested_field from orchestrator.core.resources import CoreResourceKinds @@ -13,27 +14,26 @@ deprecated_fields=["constitutivePropertyColumns", "propertyMap"], deprecated_from_version="1.3.5", removed_from_version="1.6.0", - description="Migrates CSV sample stores from v1 format (constitutivePropertyColumns at top level) to v2 format (per-experiment constitutivePropertyMap)", + description="Migrates CSV sample stores from v1 format (constitutivePropertyColumns in config) to v2 format (per-experiment constitutivePropertyMap)", + field_paths=["config.constitutivePropertyColumns", "config.experiments"], ) def migrate_csv_v1_to_v2(data: dict) -> dict: """Migrate old CSVSampleStoreDescription format to new format - This validator operates on the config level, migrating CSV sample store - configurations from v1 to v2 format. It matches the original pydantic + This validator operates on the config section of the CSV sample store, + migrating from v1 to v2 format. It matches the original pydantic validator behavior. - Old format: - config: - constitutivePropertyColumns: [...] # at config level - experiments: - - propertyMap: {...} + Old format (in config): + constitutivePropertyColumns: [...] + experiments: + - propertyMap: {...} - New format: - config: - # No constitutivePropertyColumns at config level - experiments: - - observedPropertyMap: {...} - constitutivePropertyMap: [...] + New format (in config): + # No constitutivePropertyColumns + experiments: + - observedPropertyMap: {...} + constitutivePropertyMap: [...] Args: data: The resource data dictionary @@ -45,14 +45,14 @@ def migrate_csv_v1_to_v2(data: dict) -> dict: if not isinstance(data, dict): return data - # Only operate within config + # Check if config exists if "config" not in data or not isinstance(data["config"], dict): return data config = data["config"] # Check if this is old format (has constitutivePropertyColumns in config) - if "constitutivePropertyColumns" not in config: + if not has_nested_field(data, "config.constitutivePropertyColumns"): return data # Extract and remove the constitutivePropertyColumns from config From 3551ab1130a31289b26a78d0f8cc87d14fdb0087 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 11:41:53 +0000 Subject: [PATCH 20/55] test: update tests Signed-off-by: Alessandro Pomponio --- tests/core/test_legacy_validators.py | 38 ++++++++++++------------ tests/core/test_validator_scope_fixes.py | 6 ++-- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index 14b52bea3..8e60efebd 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -16,8 +16,9 @@ class TestLegacyValidatorWithPydantic: """Test legacy validators working with pydantic models""" def setup_method(self) -> None: - """Clear the registry before each test""" - LegacyValidatorRegistry._validators = {} + """Setup method - validators are auto-discovered on import""" + # Don't clear the registry - validators are registered globally on import + # and clearing would break auto-discovery def test_validator_applied_during_model_validation(self) -> None: """Test that a legacy validator can be manually applied before pydantic validation""" @@ -69,29 +70,31 @@ def test_csv_sample_store_migration_validator(self) -> None: assert validator is not None assert validator.resource_type == CoreResourceKinds.SAMPLESTORE - # Old format CSV sample store data + # Old format CSV sample store data (with config section) old_csv_data = { "kind": "samplestore", "type": "csv", "identifier": "test_store", - "identifierColumn": "id", - "constitutivePropertyColumns": ["prop1", "prop2"], - "experiments": [ - { - "experimentIdentifier": "exp1", - "actuatorIdentifier": "act1", - "propertyMap": ["obs1", "obs2"], - } - ], + "config": { + "identifierColumn": "id", + "constitutivePropertyColumns": ["prop1", "prop2"], + "experiments": [ + { + "experimentIdentifier": "exp1", + "actuatorIdentifier": "act1", + "propertyMap": ["obs1", "obs2"], + } + ], + }, } # Apply the validator migrated_data = validator.validator_function(old_csv_data.copy()) - # Verify migration - assert "constitutivePropertyColumns" not in migrated_data - assert len(migrated_data["experiments"]) == 1 - exp = migrated_data["experiments"][0] + # Verify migration - config.constitutivePropertyColumns removed + assert "constitutivePropertyColumns" not in migrated_data["config"] + assert len(migrated_data["config"]["experiments"]) == 1 + exp = migrated_data["config"]["experiments"][0] assert "propertyMap" not in exp assert "observedPropertyMap" in exp assert exp["observedPropertyMap"] == ["obs1", "obs2"] @@ -227,7 +230,6 @@ def test_validator(data: dict) -> dict: "orchestrator.cli.utils.resources.handlers.get_sql_store", return_value=mock_sql_store, ), - patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), patch("orchestrator.cli.utils.resources.handlers.Status"), patch("orchestrator.cli.utils.resources.handlers.console_print"), ): @@ -274,7 +276,6 @@ def op_validator(data: dict) -> dict: "orchestrator.cli.utils.resources.handlers.get_sql_store", return_value=mock_sql_store, ), - patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), patch( "orchestrator.cli.utils.resources.handlers.console_print" ) as mock_print, @@ -318,7 +319,6 @@ def test_upgrade_handler_validates_validator_exists(self) -> None: "orchestrator.cli.utils.resources.handlers.get_sql_store", return_value=mock_sql_store, ), - patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), patch( "orchestrator.cli.utils.resources.handlers.console_print" ) as mock_print, diff --git a/tests/core/test_validator_scope_fixes.py b/tests/core/test_validator_scope_fixes.py index e15beca10..eb6b31a04 100644 --- a/tests/core/test_validator_scope_fixes.py +++ b/tests/core/test_validator_scope_fixes.py @@ -12,10 +12,8 @@ class TestValidatorScopeFixes: @classmethod def setup_class(cls) -> None: """Import validators once for all tests in this class""" - # Import all validators to register them - from orchestrator.cli.utils.legacy.common import import_legacy_validators - - import_legacy_validators() + # Import all validators to register them - they auto-register on import + import orchestrator.core.legacy.validators # noqa: F401 def test_discoveryspace_properties_removal_scope(self) -> None: """Verify properties_field_removal only modifies config, not resource level""" From a2d87077c94a78513c1a89c6cb732f952f7716f6 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 13:08:04 +0000 Subject: [PATCH 21/55] feat(legacy): add support for validator dependencies Signed-off-by: Alessandro Pomponio --- orchestrator/core/legacy/metadata.py | 8 + orchestrator/core/legacy/registry.py | 99 +++++- tests/core/test_validator_dependencies.py | 378 ++++++++++++++++++++++ 3 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 tests/core/test_validator_dependencies.py diff --git a/orchestrator/core/legacy/metadata.py b/orchestrator/core/legacy/metadata.py index b5ba7f792..e74cf9157 100644 --- a/orchestrator/core/legacy/metadata.py +++ b/orchestrator/core/legacy/metadata.py @@ -64,6 +64,14 @@ class LegacyValidatorMetadata(pydantic.BaseModel): ), ] + dependencies: Annotated[ + list[str], + pydantic.Field( + default_factory=list, + description="List of validator identifiers that must run before this validator", + ), + ] + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) diff --git a/orchestrator/core/legacy/registry.py b/orchestrator/core/legacy/registry.py index 61f052cba..85e9870ee 100644 --- a/orchestrator/core/legacy/registry.py +++ b/orchestrator/core/legacy/registry.py @@ -79,6 +79,99 @@ def list_all(cls) -> list[LegacyValidatorMetadata]: """ return list(cls._validators.values()) + @classmethod + def resolve_dependencies( + cls, validator_ids: list[str] + ) -> tuple[list[str], list[str]]: + """Resolve validator dependencies and return ordered list + + Uses topological sort to order validators based on their dependencies. + Detects circular dependencies. Automatically includes all transitive + dependencies. + + Args: + validator_ids: List of validator identifiers to order + + Returns: + Tuple of (ordered_validator_ids, missing_dependencies) + - ordered_validator_ids: Validators in dependency order (includes all dependencies) + - missing_dependencies: List of dependency IDs that don't exist + + Raises: + ValueError: If circular dependencies are detected + """ + # Build dependency graph - recursively add all dependencies + graph: dict[str, list[str]] = {} + in_degree: dict[str, int] = {} + missing_deps: set[str] = set() + to_process = list(validator_ids) + processed = set() + + while to_process: + vid = to_process.pop(0) + if vid in processed: + continue + processed.add(vid) + + validator = cls.get_validator(vid) + if validator is None: + continue + + # Initialize this validator in the graph + if vid not in graph: + graph[vid] = [] + in_degree[vid] = 0 + + # Process dependencies + for dep_id in validator.dependencies: + if dep_id not in cls._validators: + missing_deps.add(dep_id) + continue + + # Add dependency to graph if not already there + if dep_id not in graph: + graph[dep_id] = [] + in_degree[dep_id] = 0 + # Add to processing queue to handle transitive dependencies + to_process.append(dep_id) + + # Add edge from dependency to dependent + if vid not in graph[dep_id]: + graph[dep_id].append(vid) + + # Calculate in-degrees + for vid in graph: + validator = cls.get_validator(vid) + if validator: + for dep_id in validator.dependencies: + if dep_id in graph: + in_degree[vid] += 1 + + # Topological sort using Kahn's algorithm + queue = [vid for vid in graph if in_degree[vid] == 0] + ordered = [] + + while queue: + # Sort queue for deterministic ordering + queue.sort() + current = queue.pop(0) + ordered.append(current) + + # Reduce in-degree for dependents + for dependent in graph[current]: + in_degree[dependent] -= 1 + if in_degree[dependent] == 0: + queue.append(dependent) + + # Check for circular dependencies + if len(ordered) != len(graph): + remaining = [vid for vid in graph if vid not in ordered] + raise ValueError( + f"Circular dependency detected among validators: {', '.join(remaining)}" + ) + + return ordered, list(missing_deps) + def legacy_validator( identifier: str, @@ -88,6 +181,7 @@ def legacy_validator( removed_from_version: str, description: str, field_paths: list[str] | None = None, + dependencies: list[str] | None = None, ) -> Callable[[Callable[[dict], dict]], Callable[[dict], dict]]: """Decorator to register a legacy validator function @@ -99,6 +193,7 @@ def legacy_validator( removed_from_version: ADO version when automatic upgrade was removed description: Human-readable description of what this validator does field_paths: Optional explicit paths to fields (e.g., 'config.properties') + dependencies: Optional list of validator identifiers that must run before this one Returns: Decorator function that registers the validator @@ -111,7 +206,8 @@ def legacy_validator( field_paths=["config.specification.constitutivePropertyColumns"], deprecated_from_version="1.3.5", removed_from_version="1.6.0", - description="Migrates CSV sample stores from v1 to v2 format" + description="Migrates CSV sample stores from v1 to v2 format", + dependencies=["samplestore_kind_entitysource_to_samplestore"] ) def migrate_csv_v1_to_v2(data: dict) -> dict: # Migration logic here @@ -128,6 +224,7 @@ def decorator(func: Callable[[dict], dict]) -> Callable[[dict], dict]: description=description, validator_function=func, field_paths=field_paths or [], + dependencies=dependencies or [], ) LegacyValidatorRegistry.register(metadata) diff --git a/tests/core/test_validator_dependencies.py b/tests/core/test_validator_dependencies.py new file mode 100644 index 000000000..24ebe2599 --- /dev/null +++ b/tests/core/test_validator_dependencies.py @@ -0,0 +1,378 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Tests for validator dependency resolution and ordering""" + +from collections.abc import Generator + +import pytest + +from orchestrator.core.legacy.metadata import LegacyValidatorMetadata +from orchestrator.core.legacy.registry import LegacyValidatorRegistry +from orchestrator.core.resources import CoreResourceKinds + + +@pytest.fixture(autouse=True) +def clear_registry() -> Generator[None, None, None]: + """Clear the registry before and after each test""" + LegacyValidatorRegistry._validators.clear() + yield + LegacyValidatorRegistry._validators.clear() + + +def test_resolve_dependencies_no_dependencies() -> None: + """Test resolving validators with no dependencies""" + + def validator_a(data: dict) -> dict: + return data + + def validator_b(data: dict) -> dict: + return data + + # Register validators without dependencies + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_a", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_a"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator A", + validator_function=validator_a, + field_paths=["config.field_a"], + dependencies=[], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_b", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_b"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator B", + validator_function=validator_b, + field_paths=["config.field_b"], + dependencies=[], + ) + ) + + # Resolve dependencies + ordered, missing = LegacyValidatorRegistry.resolve_dependencies( + ["validator_a", "validator_b"] + ) + + # Should return both validators in alphabetical order (no dependencies) + assert len(ordered) == 2 + assert "validator_a" in ordered + assert "validator_b" in ordered + assert len(missing) == 0 + + +def test_resolve_dependencies_simple_chain() -> None: + """Test resolving validators with simple dependency chain""" + + def validator_a(data: dict) -> dict: + return data + + def validator_b(data: dict) -> dict: + return data + + def validator_c(data: dict) -> dict: + return data + + # Register validators: C depends on B, B depends on A + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_a", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_a"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator A", + validator_function=validator_a, + field_paths=["config.field_a"], + dependencies=[], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_b", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_b"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator B", + validator_function=validator_b, + field_paths=["config.field_b"], + dependencies=["validator_a"], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_c", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_c"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator C", + validator_function=validator_c, + field_paths=["config.field_c"], + dependencies=["validator_b"], + ) + ) + + # Resolve dependencies - only request C + ordered, missing = LegacyValidatorRegistry.resolve_dependencies(["validator_c"]) + + # Should return all three in correct order: A, B, C + assert ordered == ["validator_a", "validator_b", "validator_c"] + assert len(missing) == 0 + + +def test_resolve_dependencies_diamond() -> None: + """Test resolving validators with diamond dependency pattern""" + + def validator_a(data: dict) -> dict: + return data + + def validator_b(data: dict) -> dict: + return data + + def validator_c(data: dict) -> dict: + return data + + def validator_d(data: dict) -> dict: + return data + + # Register validators: D depends on B and C, both B and C depend on A + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_a", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_a"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator A", + validator_function=validator_a, + field_paths=["config.field_a"], + dependencies=[], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_b", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_b"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator B", + validator_function=validator_b, + field_paths=["config.field_b"], + dependencies=["validator_a"], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_c", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_c"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator C", + validator_function=validator_c, + field_paths=["config.field_c"], + dependencies=["validator_a"], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_d", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_d"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator D", + validator_function=validator_d, + field_paths=["config.field_d"], + dependencies=["validator_b", "validator_c"], + ) + ) + + # Resolve dependencies + ordered, missing = LegacyValidatorRegistry.resolve_dependencies(["validator_d"]) + + # Should return all four: A first, then B and C (in some order), then D + assert len(ordered) == 4 + assert ordered[0] == "validator_a" # A must be first + assert ordered[3] == "validator_d" # D must be last + assert "validator_b" in ordered[1:3] # B and C in middle + assert "validator_c" in ordered[1:3] + assert len(missing) == 0 + + +def test_resolve_dependencies_circular() -> None: + """Test that circular dependencies are detected""" + + def validator_a(data: dict) -> dict: + return data + + def validator_b(data: dict) -> dict: + return data + + # Register validators with circular dependency: A depends on B, B depends on A + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_a", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_a"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator A", + validator_function=validator_a, + field_paths=["config.field_a"], + dependencies=["validator_b"], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_b", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_b"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator B", + validator_function=validator_b, + field_paths=["config.field_b"], + dependencies=["validator_a"], + ) + ) + + # Should raise ValueError for circular dependency + with pytest.raises(ValueError, match="Circular dependency detected"): + LegacyValidatorRegistry.resolve_dependencies(["validator_a", "validator_b"]) + + +def test_resolve_dependencies_missing() -> None: + """Test handling of missing dependencies""" + + def validator_a(data: dict) -> dict: + return data + + # Register validator with non-existent dependency + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_a", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_a"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator A", + validator_function=validator_a, + field_paths=["config.field_a"], + dependencies=["nonexistent_validator"], + ) + ) + + # Resolve dependencies + ordered, missing = LegacyValidatorRegistry.resolve_dependencies(["validator_a"]) + + # Should return validator_a and report missing dependency + assert ordered == ["validator_a"] + assert "nonexistent_validator" in missing + + +def test_resolve_dependencies_multiple_roots() -> None: + """Test resolving validators with multiple independent roots""" + + def validator_a(data: dict) -> dict: + return data + + def validator_b(data: dict) -> dict: + return data + + def validator_c(data: dict) -> dict: + return data + + def validator_d(data: dict) -> dict: + return data + + # Register validators: C depends on A, D depends on B + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_a", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_a"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator A", + validator_function=validator_a, + field_paths=["config.field_a"], + dependencies=[], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_b", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_b"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator B", + validator_function=validator_b, + field_paths=["config.field_b"], + dependencies=[], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_c", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_c"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator C", + validator_function=validator_c, + field_paths=["config.field_c"], + dependencies=["validator_a"], + ) + ) + + LegacyValidatorRegistry.register( + LegacyValidatorMetadata( + identifier="validator_d", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["field_d"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator D", + validator_function=validator_d, + field_paths=["config.field_d"], + dependencies=["validator_b"], + ) + ) + + # Resolve dependencies + ordered, missing = LegacyValidatorRegistry.resolve_dependencies( + ["validator_c", "validator_d"] + ) + + # Should return all four validators with correct ordering + assert len(ordered) == 4 + # A must come before C + assert ordered.index("validator_a") < ordered.index("validator_c") + # B must come before D + assert ordered.index("validator_b") < ordered.index("validator_d") + assert len(missing) == 0 + + +# Made with Bob From 8b9aa5f1db0451bc15044ca57e0bea75f34868f5 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 13:16:13 +0000 Subject: [PATCH 22/55] feat(legacy): support suggesting validator dependencies Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 6 +- orchestrator/cli/utils/legacy/common.py | 92 +++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index bba5759b7..3da6ec4b9 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -86,7 +86,7 @@ def handle_validation_error_with_legacy_suggestions( import orchestrator.core.legacy.validators # noqa: F401 from orchestrator.cli.utils.legacy.common import ( extract_deprecated_fields_from_validation_error, - print_validator_suggestions, + print_validator_suggestions_with_dependencies, ) from orchestrator.core.legacy.registry import LegacyValidatorRegistry @@ -126,11 +126,11 @@ def handle_validation_error_with_legacy_suggestions( console.print(f" - {error_msg}") console.print() - print_validator_suggestions( + # Use enhanced suggestion printer with dependency information + print_validator_suggestions_with_dependencies( validators=validators, resource_type=resource_type, console=console, - show_all_validators=False, ) raise typer.Exit(1) from error diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index bcc835f3c..7dc65c8de 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -53,6 +53,98 @@ def print_validator_suggestions( console.print(f" ado upgrade {resource_cli_name} --list-legacy-validators") +def print_validator_suggestions_with_dependencies( + validators: list["LegacyValidatorMetadata"], + resource_type: "CoreResourceKinds", + console: Console, +) -> None: + """Print legacy validator suggestions with dependency information + + This enhanced version resolves dependencies and shows validators in the + correct execution order, along with dependency information. + + Args: + validators: List of applicable validators + resource_type: The resource type + console: Rich console to print to + """ + from orchestrator.core.legacy.registry import LegacyValidatorRegistry + + # Resources can be referenced by their CoreResourceKinds value or by shorthands + resource_cli_name = resource_type.value + + # Get validator identifiers + validator_ids = [v.identifier for v in validators] + + # Resolve dependencies to get correct order + try: + ordered_ids, missing_deps = LegacyValidatorRegistry.resolve_dependencies( + validator_ids + ) + except ValueError as e: + # Circular dependency detected + console.print( + f"\n[bold red]Warning:[/bold red] {e}", + style="red", + ) + # Fall back to original order + ordered_ids = validator_ids + missing_deps = [] + + # Get ordered validators (filter out None values) + ordered_validators: list[LegacyValidatorMetadata] = [] + for vid in ordered_ids: + validator = LegacyValidatorRegistry.get_validator(vid) + if validator is not None: + ordered_validators.append(validator) + + console.print("\n[bold cyan]Available legacy validators:[/bold cyan]\n") + + for i, validator in enumerate(ordered_validators, 1): + # Show execution order + console.print(f" {i}. [green]{validator.identifier}[/green]") + console.print(f" {validator.description}") + console.print(f" Handles: {', '.join(validator.deprecated_fields)}") + console.print(f" Deprecated: v{validator.deprecated_from_version}") + + # Show dependencies if any + if validator.dependencies: + dep_names = [] + for dep_id in validator.dependencies: + dep_validator = LegacyValidatorRegistry.get_validator(dep_id) + if dep_validator: + dep_names.append(dep_validator.identifier) + else: + dep_names.append(f"{dep_id} [red](missing)[/red]") + console.print(f" Dependencies: {', '.join(dep_names)}") + + console.print() + + # Warn about missing dependencies + if missing_deps: + console.print( + f"[bold yellow]Warning:[/bold yellow] Some dependencies are missing: {', '.join(missing_deps)}\n" + ) + + console.print("[bold magenta]To upgrade using legacy validators:[/bold magenta]") + + # Build command with all validators in correct order + validator_args = " ".join( + f"--apply-legacy-validator {v.identifier}" for v in ordered_validators + ) + console.print(f" ado upgrade {resource_cli_name} {validator_args}") + console.print() + + # Show note about automatic dependency resolution + if len(ordered_validators) > len(validators): + console.print( + "[dim]Note: Additional validators were included to satisfy dependencies[/dim]\n" + ) + + console.print("[bold magenta]To list all legacy validators:[/bold magenta]") + console.print(f" ado upgrade {resource_cli_name} --list-legacy-validators") + + # Made with Bob From 193c1a62a6ac6322b62ac1aa2b4e1bf37df74a11 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 13:27:15 +0000 Subject: [PATCH 23/55] feat(cli): support auto-handling validator dependencies Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/resources/handlers.py | 47 +++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 0c7895e62..9d85f4a1e 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -257,19 +257,54 @@ def handle_ado_upgrade( if parameters.apply_legacy_validator: from orchestrator.core.legacy.registry import LegacyValidatorRegistry - legacy_validators = [] + # Validate all validator IDs exist + invalid_validators = [] for validator_id in parameters.apply_legacy_validator: validator = LegacyValidatorRegistry.get_validator(validator_id) if validator is None: + invalid_validators.append(validator_id) + + if invalid_validators: + console_print( + f"{ERROR}Unknown legacy validator(s): {', '.join(invalid_validators)}", + stderr=True, + ) + raise typer.Exit(1) + + # Resolve dependencies and order validators + try: + ordered_ids, missing_deps = LegacyValidatorRegistry.resolve_dependencies( + parameters.apply_legacy_validator + ) + + if missing_deps: console_print( - f"{ERROR}Unknown legacy validator: {validator_id}", stderr=True + f"{ERROR}Missing validator dependencies: {', '.join(missing_deps)}", + stderr=True, ) raise typer.Exit(1) - legacy_validators.append(validator) - logger.debug( - f"Selected legacy validators: {[v.identifier for v in legacy_validators]}" - ) + # Get validators in correct order + legacy_validators = [] + for validator_id in ordered_ids: + validator = LegacyValidatorRegistry.get_validator(validator_id) + if validator is not None: + legacy_validators.append(validator) + + # Log the ordering + if len(ordered_ids) > len(parameters.apply_legacy_validator): + logger.info( + f"Auto-included dependencies: {[vid for vid in ordered_ids if vid not in parameters.apply_legacy_validator]}" + ) + + logger.debug( + f"Validators in execution order: {[v.identifier for v in legacy_validators]}" + ) + + except ValueError as e: + # Circular dependency detected + console_print(f"{ERROR}{e}", stderr=True) + raise typer.Exit(1) from e sql_store = get_sql_store( project_context=parameters.ado_configuration.project_context From c98c936a07eca29c4bc84a9a8b77c8446473e279 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 13:35:34 +0000 Subject: [PATCH 24/55] docs(website): add docs for legacy validators Signed-off-by: Alessandro Pomponio --- website/docs/cli/legacy-validators.md | 393 ++++++++++++++++++++++++++ website/mkdocs.yml | 27 +- 2 files changed, 407 insertions(+), 13 deletions(-) create mode 100644 website/docs/cli/legacy-validators.md diff --git a/website/docs/cli/legacy-validators.md b/website/docs/cli/legacy-validators.md new file mode 100644 index 000000000..2f821b9c7 --- /dev/null +++ b/website/docs/cli/legacy-validators.md @@ -0,0 +1,393 @@ +# Legacy Validators + +Legacy validators help you upgrade old resource files that use deprecated fields +or formats. This guide covers the legacy validator system and its advanced +features. + +## Overview + +The legacy validator system provides: + +- **Automatic dependency resolution**: Validators can depend on other validators +- **Smart error messages**: Get helpful suggestions when validation fails +- **Progress tracking**: Visual feedback during upgrade operations +- **Automatic ordering**: Validators run in the correct order automatically +- **Dry-run mode**: Preview changes before applying them + +## Quick Start + +### List Available Validators + +View all legacy validators: + +```bash +ado legacy list +``` + +View validators for a specific resource type: + +```bash +ado legacy list samplestore +``` + +### Get Validator Information + +Get detailed information about a specific validator: + +```bash +ado legacy info discoveryspace_properties_field_removal +``` + +This shows: + +- Description of what the validator does +- Which deprecated fields it handles +- Version information +- Usage examples + +### Upgrade Resources + +#### Upgrade Resources in Metastore + +Apply a legacy validator to resources in your metastore: + +```bash +ado upgrade discoveryspace --apply-legacy-validator discoveryspace_properties_field_removal +``` + +Apply multiple validators (they will be automatically ordered): + +```bash +ado upgrade samplestore \ + --apply-legacy-validator samplestore_kind_entitysource_to_samplestore \ + --apply-legacy-validator samplestore_module_type_entitysource_to_samplestore +``` + +#### Upgrade Local YAML Files + +Upgrade local YAML files without loading them into the metastore: + +```bash +ado legacy upgrade --file my-resource.yaml \ + --apply-legacy-validator discoveryspace_properties_field_removal +``` + +Upgrade multiple files: + +```bash +ado legacy upgrade \ + --file resource1.yaml \ + --file resource2.yaml \ + --apply-legacy-validator validator_name +``` + +## Advanced Features + +### Automatic Dependency Resolution + +Validators can depend on other validators. When you specify a validator, the +system automatically: + +1. Includes all required dependencies +2. Orders validators correctly using topological sort +3. Notifies you when dependencies are auto-included + +**Example:** + +If validator B depends on validator A, you only need to specify B: + +```bash +ado upgrade samplestore --apply-legacy-validator validator_b +``` + +The system will automatically: + +- Include validator A +- Run A before B +- Show you: "Auto-included dependencies: validator_a" + +### Circular Dependency Detection + +The system detects circular dependencies and provides clear error messages: + +```text +Error: Circular dependency detected among validators: validator_a, validator_b +``` + +### Enhanced Error Messages + +When validation fails, you get: + +1. **Detailed field errors**: Exact fields that failed and why +2. **Applicable validators**: List of validators that can fix the issues +3. **Dependency information**: Which validators depend on others +4. **Ready-to-use commands**: Complete commands you can copy-paste + +**Example error output:** + +```text +Validation Error in discoveryspace 'my-space' + +Fields with validation errors: 2 field(s) + +Error details: + • config.properties: + - Extra inputs are not permitted + • config.entitySourceIdentifier: + - Field required + +Available legacy validators: + + 1. discoveryspace_properties_field_removal + Removes the deprecated 'properties' field + Handles: properties + Deprecated: v0.10.1 + + 2. discoveryspace_entitysource_to_samplestore + Renames 'entitySourceIdentifier' to 'sampleStoreIdentifier' + Handles: entitySourceIdentifier + Deprecated: v0.9.6 + Dependencies: discoveryspace_properties_field_removal + +To upgrade using legacy validators: + ado upgrade discoveryspace \ + --apply-legacy-validator discoveryspace_properties_field_removal \ + --apply-legacy-validator discoveryspace_entitysource_to_samplestore +``` + +### Progress Tracking + +When upgrading local files, you see: + +- Overall progress bar for files +- Per-file validator progress +- Real-time status updates +- Clear success/failure indicators + +**Example:** + +```text +Processing 3 file(s) with 2 validator(s)... + +⠋ Processing file1.yaml... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 33% + Applying validator_a... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 50% + +File: file1.yaml +Applied validators: validator_a, validator_b +✓ Upgraded: file1.yaml +``` + +### Dry-Run Mode + +Preview changes without modifying files: + +```bash +ado legacy upgrade --file my-resource.yaml \ + --apply-legacy-validator validator_name \ + --dry-run +``` + +This shows: + +- Which validators would be applied +- Preview of the modified YAML +- No files are actually changed + +### Backup Creation + +When upgrading files in-place, backups are created automatically: + +```bash +ado legacy upgrade --file my-resource.yaml \ + --apply-legacy-validator validator_name +``` + +Creates: `my-resource.yaml.bak` + +Disable backups: + +```bash +ado legacy upgrade --file my-resource.yaml \ + --apply-legacy-validator validator_name \ + --no-backup +``` + +### Output to Different Directory + +Upgrade files to a different directory: + +```bash +ado legacy upgrade \ + --file old/resource.yaml \ + --apply-legacy-validator validator_name \ + --output-dir upgraded/ +``` + +## Available Validators + +### Discovery Space Validators + +#### discoveryspace_properties_field_removal + +- **Handles**: `properties` +- **Description**: Removes the deprecated 'properties' field +- **Deprecated from**: v0.10.1 +- **Removed from**: v1.0.0 + +#### discoveryspace_entitysource_to_samplestore + +- **Handles**: `entitySourceIdentifier` +- **Description**: Renames 'entitySourceIdentifier' to 'sampleStoreIdentifier' +- **Deprecated from**: v0.9.6 +- **Removed from**: v1.0.0 + +### Operation Validators + +#### operation_actuators_field_removal + +- **Handles**: `actuators` +- **Description**: Removes the deprecated 'actuators' field +- **Deprecated from**: v0.9.6 +- **Removed from**: v1.0.0 +- **See**: + [Operation Configuration](../resources/operation.md#the-operation-configuration-yaml) + +#### randomwalk_mode_to_sampler_config + +- **Handles**: `mode`, `grouping`, `samplerType` +- **Description**: Migrates random_walk parameters to nested 'samplerConfig' +- **Deprecated from**: v1.0.1 +- **Removed from**: v1.2 +- **See**: + [Random Walk Configuration](../operators/random-walk.md#configuring-a-randomwalk) + +### Sample Store Validators + +#### samplestore_kind_entitysource_to_samplestore + +- **Handles**: `kind` +- **Description**: Converts resource kind from 'entitysource' to 'samplestore' +- **Deprecated from**: v0.9.6 +- **Removed from**: v1.0.0 + +#### samplestore_module_type_entitysource_to_samplestore + +- **Handles**: `moduleType` +- **Description**: Converts moduleType from 'entity_source' to 'sample_store' +- **Deprecated from**: v0.9.6 +- **Removed from**: v1.0.0 + +#### samplestore_module_class_entitysource_to_samplestore + +- **Handles**: `moduleClass` +- **Description**: Converts moduleClass from EntitySource to SampleStore naming +- **Deprecated from**: v0.9.6 +- **Removed from**: v1.0.0 + +#### samplestore_module_name_entitysource_to_samplestore + +- **Handles**: `moduleName` +- **Description**: Updates module paths from entitysource to samplestore +- **Deprecated from**: v0.9.6 +- **Removed from**: v1.0.0 + +#### csv_constitutive_columns_migration + +- **Handles**: `constitutivePropertyColumns`, `propertyMap` +- **Description**: Migrates CSV sample stores from v1 to v2 format +- **Deprecated from**: v1.3.5 +- **Removed from**: v1.6.0 + +## Best Practices + +### 1. Use `ado legacy list` First + +Before upgrading, check which validators are available: + +```bash +ado legacy list samplestore +``` + +### 2. Get Detailed Information + +Use `ado legacy info` to understand what a validator does: + +```bash +ado legacy info csv_constitutive_columns_migration +``` + +### 3. Test with Dry-Run + +Always test with `--dry-run` first: + +```bash +ado legacy upgrade --file my-resource.yaml \ + --apply-legacy-validator validator_name \ + --dry-run +``` + +### 4. Let Dependencies Auto-Resolve + +Don't manually specify dependencies - the system handles it: + +```bash +# Good - just specify what you need +ado upgrade samplestore --apply-legacy-validator validator_b + +# Unnecessary - dependencies are automatic +ado upgrade samplestore \ + --apply-legacy-validator validator_a \ + --apply-legacy-validator validator_b +``` + +### 5. Keep Backups + +When upgrading important files, keep the default backup behavior: + +```bash +# Backups created automatically +ado legacy upgrade --file important.yaml \ + --apply-legacy-validator validator_name +``` + +## Troubleshooting + +### Validator Not Found + +If you get "Unknown legacy validator": + +1. Check the validator name: `ado legacy list` +2. Ensure you're using the full identifier +3. Check for typos + +### Circular Dependency Error + +If you see "Circular dependency detected": + +1. This indicates a bug in validator definitions +2. Report the issue with the validator names +3. Use validators individually as a workaround + +### Missing Dependencies + +If you see "Missing validator dependencies": + +1. The validator depends on another validator that doesn't exist +2. This indicates a configuration issue +3. Report the issue with details + +### Validation Still Fails + +If validation fails after applying validators: + +1. Check if you applied all suggested validators +2. Verify the resource format matches expectations +3. Check for additional deprecated fields +4. Use `ado legacy list` to find other applicable validators + +## See Also + +- [Resource Upgrade Command](../cli/upgrade.md) +- [Discovery Space Resources](../resources/discoveryspace.md) +- [Sample Store Resources](../resources/samplestore.md) +- [Operation Resources](../resources/operation.md) diff --git a/website/mkdocs.yml b/website/mkdocs.yml index dc256524d..0676e4c71 100644 --- a/website/mkdocs.yml +++ b/website/mkdocs.yml @@ -4,15 +4,15 @@ site_url: "https://ibm.github.io/ado" site_description: "Documentation website for https://github.com/ibm/ado" site_author: "The ado authors" # Repository Information -repo_name: 'ibm/ado' -repo_url: 'https://github.com/ibm/ado' -edit_uri: 'edit/main/website/docs' -docs_dir: 'docs' -site_dir: 'site' -remote_branch: 'gh-pages' -remote_name: 'origin' +repo_name: "ibm/ado" +repo_url: "https://github.com/ibm/ado" +edit_uri: "edit/main/website/docs" +docs_dir: "docs" +site_dir: "site" +remote_branch: "gh-pages" +remote_name: "origin" # Server info -dev_addr: '127.0.0.1:8000' +dev_addr: "127.0.0.1:8000" # Copyright copyright: Copyright IBM Research. # Extra links as icons at the bottom of the page @@ -31,8 +31,8 @@ theme: include_search_page: true search_index_only: true custom_dir: theme - favicon: 'favicon.ico' - logo: 'logo-ibm.png' + favicon: "favicon.ico" + logo: "logo-ibm.png" font: false palette: # Palette toggle for automatic mode @@ -78,7 +78,7 @@ theme: - search.suggest - toc.integrate # Integrate page TOC in left sidebar - wider page icon: - edit: 'material/pencil-outline' + edit: "material/pencil-outline" markdown_extensions: admonition: {} # def_list: {} @@ -95,7 +95,6 @@ markdown_extensions: pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - # Better list support: https://facelessuser.github.io/pymdown-extensions/extensions/fancylists/ pymdownx.fancylists: {} # Code syntax highlighting: https://facelessuser.github.io/pymdown-extensions/extensions/highlight/ pymdownx.highlight: {} @@ -110,7 +109,7 @@ markdown_extensions: # Strip HTML: https://facelessuser.github.io/pymdown-extensions/extensions/striphtml/ pymdownx.striphtml: {} # Better fencing: https://facelessuser.github.io/pymdown-extensions/extensions/superfences/ - pymdownx.superfences: + pymdownx.superfences: # Better list support: https://facelessuser.github.io/pymdown-extensions/extensions/fancylists/ preserve_tabs: true # Tabs: https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ pymdownx.tabbed: @@ -144,6 +143,8 @@ nav: - Developing ado: getting-started/developing.md - Roadmap: getting-started/roadmap.md - Contributing: getting-started/contributing.md + - CLI Reference: + - Legacy Validators: cli/legacy-validators.md - Examples: - examples/examples.md - Taking a random walk: examples/random-walk.md From dc649091eb394773c2013719bfe75f72f8abd57d Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Tue, 24 Mar 2026 15:31:25 +0000 Subject: [PATCH 25/55] test: fix tests Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/resources/handlers.py | 16 +++++++- tests/core/test_legacy_validators.py | 29 +++++++++------ tests/core/test_upgrade_transaction_safety.py | 3 -- tests/core/test_validator_scope_fixes.py | 37 ++++++++++++++++--- 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 9d85f4a1e..3eec6a07f 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -257,12 +257,17 @@ def handle_ado_upgrade( if parameters.apply_legacy_validator: from orchestrator.core.legacy.registry import LegacyValidatorRegistry - # Validate all validator IDs exist + # Validate all validator IDs exist and match resource type invalid_validators = [] + mismatched_validators = [] for validator_id in parameters.apply_legacy_validator: validator = LegacyValidatorRegistry.get_validator(validator_id) if validator is None: invalid_validators.append(validator_id) + elif validator.resource_type != resource_type: + mismatched_validators.append( + (validator_id, validator.resource_type, resource_type) + ) if invalid_validators: console_print( @@ -271,6 +276,15 @@ def handle_ado_upgrade( ) raise typer.Exit(1) + if mismatched_validators: + for validator_id, validator_type, expected_type in mismatched_validators: + console_print( + f"{ERROR}Validator '{validator_id}' is for {validator_type.value} resources, " + f"but you are upgrading {expected_type.value} resources", + stderr=True, + ) + raise typer.Exit(1) + # Resolve dependencies and order validators try: ordered_ids, missing_deps = LegacyValidatorRegistry.resolve_dependencies( diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index 8e60efebd..6fdcb7745 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -209,14 +209,13 @@ def test_validator(data: dict) -> dict: mock_validated_resource = MagicMock() mock_resource_class.model_validate.return_value = mock_validated_resource - # Create mock resource instance - mock_resource = MagicMock() - mock_resource.model_dump.return_value = {"old_field": "test_value"} - # Configure type() to return our mock class - type(mock_resource).model_validate = mock_resource_class.model_validate - mock_sql_store = MagicMock() - mock_sql_store.getResourcesOfKind.return_value = {"res1": mock_resource} + # Mock getResourceIdentifiersOfKind to return identifiers + mock_sql_store.getResourceIdentifiersOfKind.return_value = { + "IDENTIFIER": ["res1"] + } + # Mock getResourceRaw to return raw dict data + mock_sql_store.getResourceRaw.return_value = {"old_field": "test_value"} # Mock parameters mock_params = MagicMock() @@ -224,7 +223,7 @@ def test_validator(data: dict) -> dict: mock_params.list_legacy_validators = False mock_params.ado_configuration.project_context = "test_context" - # Patch dependencies + # Patch dependencies including kindmap with ( patch( "orchestrator.cli.utils.resources.handlers.get_sql_store", @@ -232,6 +231,10 @@ def test_validator(data: dict) -> dict: ), patch("orchestrator.cli.utils.resources.handlers.Status"), patch("orchestrator.cli.utils.resources.handlers.console_print"), + patch( + "orchestrator.core.kindmap", + {CoreResourceKinds.SAMPLESTORE.value: mock_resource_class}, + ), ): from orchestrator.cli.utils.resources.handlers import ( handle_ado_upgrade, @@ -244,7 +247,8 @@ def test_validator(data: dict) -> dict: ) # Verify the resource was processed - mock_resource.model_dump.assert_called_once() + mock_sql_store.getResourceRaw.assert_called_once_with("res1") + mock_resource_class.model_validate.assert_called_once() mock_sql_store.updateResource.assert_called_once() def test_upgrade_handler_validates_validator_resource_type(self) -> None: @@ -295,12 +299,14 @@ def op_validator(data: dict) -> dict: assert exc_info.value.exit_code == 1 - # Verify error message was printed + # Verify error message was printed with correct resource type mismatch mock_print.assert_called() call_args = str(mock_print.call_args) assert "operation_validator" in call_args assert "operation" in call_args.lower() assert "samplestore" in call_args.lower() + # Check for the specific error message format + assert "is for" in call_args.lower() or "upgrading" in call_args.lower() def test_upgrade_handler_validates_validator_exists(self) -> None: """Test that upgrade handler validates validator exists""" @@ -342,7 +348,8 @@ def test_upgrade_handler_validates_validator_exists(self) -> None: mock_print.assert_called() call_args = str(mock_print.call_args) assert "nonexistent_validator" in call_args - assert "not found" in call_args.lower() + # Check for "unknown" instead of "not found" to match actual error message + assert "unknown" in call_args.lower() class TestValidatorDataIntegrity: diff --git a/tests/core/test_upgrade_transaction_safety.py b/tests/core/test_upgrade_transaction_safety.py index 895a3b526..ec8a5279e 100644 --- a/tests/core/test_upgrade_transaction_safety.py +++ b/tests/core/test_upgrade_transaction_safety.py @@ -99,7 +99,6 @@ def track_update(resource: MagicMock) -> None: "orchestrator.core.kindmap", {"samplestore": mock_resource_class}, ), - patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), patch("orchestrator.cli.utils.resources.handlers.Status"), patch("orchestrator.cli.utils.resources.handlers.console_print"), ): @@ -198,7 +197,6 @@ def mock_validate(data: dict) -> MagicMock: "orchestrator.core.kindmap", {"samplestore": mock_resource_class}, ), - patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), patch("orchestrator.cli.utils.resources.handlers.Status"), patch( "orchestrator.cli.utils.resources.handlers.console_print" @@ -253,7 +251,6 @@ def test_validator(data: dict) -> dict: "orchestrator.cli.utils.resources.handlers.get_sql_store", return_value=mock_sql_store, ), - patch("orchestrator.cli.utils.legacy.common.import_legacy_validators"), patch("orchestrator.cli.utils.resources.handlers.Status"), patch( "orchestrator.cli.utils.resources.handlers.console_print" diff --git a/tests/core/test_validator_scope_fixes.py b/tests/core/test_validator_scope_fixes.py index eb6b31a04..f46097e62 100644 --- a/tests/core/test_validator_scope_fixes.py +++ b/tests/core/test_validator_scope_fixes.py @@ -3,17 +3,44 @@ """Tests for Phase 1 validator scope fixes - verifying validators only operate on config level""" +import importlib + from orchestrator.core.legacy.registry import LegacyValidatorRegistry class TestValidatorScopeFixes: """Test that validators correctly operate only on config level after Phase 1 fixes""" - @classmethod - def setup_class(cls) -> None: - """Import validators once for all tests in this class""" - # Import all validators to register them - they auto-register on import - import orchestrator.core.legacy.validators # noqa: F401 + def setup_method(self) -> None: + """Clear registry and reload validators before each test for isolation""" + # Clear the registry to ensure clean state + LegacyValidatorRegistry._validators = {} + # Reload all validator module files to re-register them + import orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore + import orchestrator.core.legacy.validators.discoveryspace.properties_field_removal + import orchestrator.core.legacy.validators.operation.actuators_field_removal + import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore + import orchestrator.core.legacy.validators.samplestore.entitysource_migrations + import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration + + importlib.reload( + orchestrator.core.legacy.validators.discoveryspace.properties_field_removal + ) + importlib.reload( + orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore + ) + importlib.reload( + orchestrator.core.legacy.validators.operation.actuators_field_removal + ) + importlib.reload( + orchestrator.core.legacy.validators.resource.entitysource_to_samplestore + ) + importlib.reload( + orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration + ) + importlib.reload( + orchestrator.core.legacy.validators.samplestore.entitysource_migrations + ) def test_discoveryspace_properties_removal_scope(self) -> None: """Verify properties_field_removal only modifies config, not resource level""" From bc22fb1951c645140b96cd6bee73bd63020a661a Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 10:05:58 +0000 Subject: [PATCH 26/55] refactor(cli): use full paths for matching validators Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 12 +- orchestrator/core/legacy/registry.py | 22 ++++ tests/core/test_legacy_registry.py | 145 +++++++++++++++++++++++- 3 files changed, 174 insertions(+), 5 deletions(-) diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index 3da6ec4b9..b04968637 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -99,11 +99,17 @@ def handle_validation_error_with_legacy_suggestions( console_print(f"Validation error: {error}", stderr=True) raise typer.Exit(1) from error - # Find applicable legacy validators using leaf field names for better matching - validators = LegacyValidatorRegistry.find_validators_for_fields( - resource_type=resource_type, field_names=leaf_field_names + # Find applicable legacy validators using full field paths for precise matching + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + resource_type=resource_type, field_paths=full_field_paths ) + # Fallback to leaf field name matching if no validators found with full paths + if not validators: + validators = LegacyValidatorRegistry.find_validators_for_fields( + resource_type=resource_type, field_names=leaf_field_names + ) + if not validators: # No legacy validators available, show standard error console_print(f"Validation error: {error}", stderr=True) diff --git a/orchestrator/core/legacy/registry.py b/orchestrator/core/legacy/registry.py index 85e9870ee..ae2142aa1 100644 --- a/orchestrator/core/legacy/registry.py +++ b/orchestrator/core/legacy/registry.py @@ -70,6 +70,28 @@ def find_validators_for_fields( if any(field in v.deprecated_fields for field in field_names) ] + @classmethod + def find_validators_for_field_paths( + cls, resource_type: CoreResourceKinds, field_paths: set[str] + ) -> list[LegacyValidatorMetadata]: + """Find validators that handle specific field paths + + Matches validators based on their declared field_paths, providing + more precise matching than deprecated_fields (leaf names). + + Args: + resource_type: The resource type to filter by + field_paths: Set of full dotted paths (e.g., 'config.properties') + + Returns: + List of validator metadata that handle any of the specified paths + """ + return [ + v + for v in cls.get_validators_for_resource(resource_type) + if any(path in v.field_paths for path in field_paths) + ] + @classmethod def list_all(cls) -> list[LegacyValidatorMetadata]: """List all registered validators diff --git a/tests/core/test_legacy_registry.py b/tests/core/test_legacy_registry.py index ea4d2827e..36060c054 100644 --- a/tests/core/test_legacy_registry.py +++ b/tests/core/test_legacy_registry.py @@ -218,6 +218,92 @@ def dummy_validator(data: dict) -> dict: ) assert len(validators) == 0 + def test_find_validators_for_field_paths(self) -> None: + """Test finding validators that handle specific field paths""" + + def dummy_validator(data: dict) -> dict: + return data + + # Register validators with different field paths + metadata1 = LegacyValidatorMetadata( + identifier="validator1", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field1", "field2"], + field_paths=["config.field1", "config.field2"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator 1", + validator_function=dummy_validator, + ) + + metadata2 = LegacyValidatorMetadata( + identifier="validator2", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_fields=["field3"], + field_paths=["config.specification.field3"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator 2", + validator_function=dummy_validator, + ) + + metadata3 = LegacyValidatorMetadata( + identifier="validator3", + resource_type=CoreResourceKinds.DISCOVERYSPACE, + deprecated_fields=["properties"], + field_paths=["config.properties"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Validator 3", + validator_function=dummy_validator, + ) + + LegacyValidatorRegistry.register(metadata1) + LegacyValidatorRegistry.register(metadata2) + LegacyValidatorRegistry.register(metadata3) + + # Find validators for single full path + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.SAMPLESTORE, {"config.field1"} + ) + assert len(validators) == 1 + assert validators[0].identifier == "validator1" + + # Find validators for nested path + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.SAMPLESTORE, {"config.specification.field3"} + ) + assert len(validators) == 1 + assert validators[0].identifier == "validator2" + + # Find validators for multiple paths + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.SAMPLESTORE, + {"config.field1", "config.specification.field3"}, + ) + assert len(validators) == 2 + validator_ids = {v.identifier for v in validators} + assert validator_ids == {"validator1", "validator2"} + + # Find validators for non-existent path + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.SAMPLESTORE, {"config.nonexistent"} + ) + assert len(validators) == 0 + + # Verify it doesn't match on leaf names alone (more specific than find_validators_for_fields) + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.SAMPLESTORE, {"field1"} # Just leaf name, not full path + ) + assert len(validators) == 0 + + # Verify resource type filtering works + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.DISCOVERYSPACE, {"config.properties"} + ) + assert len(validators) == 1 + assert validators[0].identifier == "validator3" + def test_list_all(self) -> None: """Test listing all validators""" @@ -250,6 +336,62 @@ def dummy_validator(data: dict) -> dict: all_validators = LegacyValidatorRegistry.list_all() assert len(all_validators) == 2 + def test_field_path_matching_with_real_validators(self) -> None: + """Integration test: verify field path matching works with real validators""" + # Import validators to trigger registration + import orchestrator.core.legacy.validators # noqa: F401 + + # Test 1: discoveryspace properties field should match the properties_field_removal validator + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.DISCOVERYSPACE, {"config.properties"} + ) + assert len(validators) >= 1 + validator_ids = {v.identifier for v in validators} + assert "discoveryspace_properties_field_removal" in validator_ids + + # Test 2: operation actuators field should match the actuators_field_removal validator + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.OPERATION, {"config.actuators"} + ) + assert len(validators) >= 1 + validator_ids = {v.identifier for v in validators} + assert "operation_actuators_field_removal" in validator_ids + + # Test 3: operation parameters.mode should match randomwalk validator + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.OPERATION, {"config.parameters.mode"} + ) + assert len(validators) >= 1 + validator_ids = {v.identifier for v in validators} + assert "randomwalk_mode_to_sampler_config" in validator_ids + + # Test 4: samplestore config.moduleType should match the module_type validator + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.SAMPLESTORE, {"config.moduleType"} + ) + assert len(validators) >= 1 + validator_ids = {v.identifier for v in validators} + assert "samplestore_module_type_entitysource_to_samplestore" in validator_ids + + # Test 5: samplestore kind field should match the kind validator + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.SAMPLESTORE, {"kind"} + ) + assert len(validators) >= 1 + validator_ids = {v.identifier for v in validators} + assert "samplestore_kind_entitysource_to_samplestore" in validator_ids + + # Test 6: Multiple paths should return multiple validators + validators = LegacyValidatorRegistry.find_validators_for_field_paths( + CoreResourceKinds.SAMPLESTORE, + {"config.moduleType", "config.moduleClass", "config.moduleName"}, + ) + assert len(validators) >= 3 + validator_ids = {v.identifier for v in validators} + assert "samplestore_module_type_entitysource_to_samplestore" in validator_ids + assert "samplestore_module_class_entitysource_to_samplestore" in validator_ids + assert "samplestore_module_name_entitysource_to_samplestore" in validator_ids + class TestLegacyValidatorDecorator: """Test the @legacy_validator decorator""" @@ -331,5 +473,4 @@ def transform_validator(data: dict) -> dict: assert "old_field" not in result2 assert result2["new_field"] == "another_value" - -# Made with Bob + # Made with Bob From 7cdc2140e6d6f020d517718332f7135464a4cb7e Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 10:38:43 +0000 Subject: [PATCH 27/55] refactor(core): only support fully qualified field names in legacy validator registry Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 21 ++-- orchestrator/cli/utils/legacy/common.py | 62 +++++---- orchestrator/cli/utils/legacy/list.py | 6 +- orchestrator/cli/utils/resources/handlers.py | 21 ++-- orchestrator/core/legacy/metadata.py | 10 +- orchestrator/core/legacy/registry.py | 44 ++----- .../entitysource_to_samplestore.py | 3 +- .../properties_field_removal.py | 3 +- .../operation/actuators_field_removal.py | 3 +- .../randomwalk_mode_to_sampler_config.py | 9 +- .../resource/entitysource_to_samplestore.py | 3 +- .../samplestore/entitysource_migrations.py | 9 +- .../samplestore/v1_to_v2_csv_migration.py | 7 +- tests/core/test_legacy_registry.py | 118 +++++------------- tests/core/test_legacy_validators.py | 46 ++++--- tests/core/test_upgrade_transaction_safety.py | 6 +- tests/core/test_validator_dependencies.py | 48 +++---- 17 files changed, 164 insertions(+), 255 deletions(-) diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index b04968637..8eb7f5c55 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -90,26 +90,21 @@ def handle_validation_error_with_legacy_suggestions( ) from orchestrator.core.legacy.registry import LegacyValidatorRegistry - # Extract field paths, error details, and leaf field names from validation error - full_field_paths, field_errors, leaf_field_names = ( + # Extract field paths and error details from validation error + fully_qualified_deprecated_field_paths, field_errors = ( extract_deprecated_fields_from_validation_error(error) ) - if not full_field_paths: + if not fully_qualified_deprecated_field_paths: # No fields extracted, show standard error console_print(f"Validation error: {error}", stderr=True) raise typer.Exit(1) from error # Find applicable legacy validators using full field paths for precise matching - validators = LegacyValidatorRegistry.find_validators_for_field_paths( - resource_type=resource_type, field_paths=full_field_paths + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + resource_type=resource_type, + fully_qualified_deprecated_field_paths=fully_qualified_deprecated_field_paths, ) - # Fallback to leaf field name matching if no validators found with full paths - if not validators: - validators = LegacyValidatorRegistry.find_validators_for_fields( - resource_type=resource_type, field_names=leaf_field_names - ) - if not validators: # No legacy validators available, show standard error console_print(f"Validation error: {error}", stderr=True) @@ -122,11 +117,11 @@ def handle_validation_error_with_legacy_suggestions( f"\n[bold red]Validation Error[/bold red] in {resource_type.value}{resource_id_str}" ) console.print( - f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(full_field_paths)} field(s)[/yellow]" + f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(fully_qualified_deprecated_field_paths)} field(s)[/yellow]" ) # Show detailed error messages for each field path console.print("\n[bold]Error details:[/bold]") - for field_path in sorted(full_field_paths): + for field_path in sorted(fully_qualified_deprecated_field_paths): console.print(f" • [cyan]{field_path}[/cyan]:") for error_msg in field_errors.get(field_path, []): console.print(f" - {error_msg}") diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index 7dc65c8de..0274ac78e 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -36,7 +36,9 @@ def print_validator_suggestions( for validator in validators: console.print(f" • [green]{validator.identifier}[/green]") console.print(f" {validator.description}") - console.print(f" Handles: {', '.join(validator.deprecated_fields)}") + console.print( + f" Handles: {', '.join(validator.fully_qualified_deprecated_field_paths)}" + ) console.print(f" Deprecated: v{validator.deprecated_from_version}") console.print() @@ -104,7 +106,9 @@ def print_validator_suggestions_with_dependencies( # Show execution order console.print(f" {i}. [green]{validator.identifier}[/green]") console.print(f" {validator.description}") - console.print(f" Handles: {', '.join(validator.deprecated_fields)}") + console.print( + f" Handles: {', '.join(validator.fully_qualified_deprecated_field_paths)}" + ) console.print(f" Deprecated: v{validator.deprecated_from_version}") # Show dependencies if any @@ -150,31 +154,25 @@ def print_validator_suggestions_with_dependencies( def extract_deprecated_fields_from_validation_error( error: pydantic.ValidationError, -) -> tuple[set[str], dict[str, list[str]], set[str]]: - """Extract field names and error details from pydantic validation errors +) -> tuple[set[str], dict[str, list[str]]]: + """Extract field paths and error details from pydantic validation errors Args: error: The pydantic validation error Returns: - Tuple of (full field paths, field error details mapping, leaf field names) + Tuple of (full field paths, field error details mapping) - full field paths: Set of full dotted paths like 'config.specification.module.moduleType' - field error details: Maps full field path to list of error messages - - leaf field names: Set of just the final field names for validator matching """ - full_field_paths: set[str] = set() + fully_qualified_deprecated_field_paths: set[str] = set() field_errors: dict[str, list[str]] = {} - leaf_field_names: set[str] = set() for err in error.errors(): if err.get("loc"): # Build the full dotted path from the location tuple full_path = ".".join(str(loc) for loc in err["loc"]) - full_field_paths.add(full_path) - - # Get the leaf field name (last element) for validator matching - leaf_field = str(err["loc"][-1]) - leaf_field_names.add(leaf_field) + fully_qualified_deprecated_field_paths.add(full_path) # Store the error message for this field path if full_path not in field_errors: @@ -187,26 +185,26 @@ def extract_deprecated_fields_from_validation_error( field_errors[full_path].append(msg) - return full_field_paths, field_errors, leaf_field_names + return fully_qualified_deprecated_field_paths, field_errors def extract_deprecated_fields_from_value_error( error: ValueError, resource_type: "CoreResourceKinds", -) -> tuple[set[str], dict[str, list[str]], set[str]]: - """Extract field names from ValueError containing pydantic validation errors +) -> tuple[set[str], dict[str, list[str]]]: + """Extract field paths from ValueError containing pydantic validation errors This function attempts to extract the underlying pydantic ValidationError - from a ValueError and extract field names from it. If that fails, it falls - back to simple string matching on the error message using known deprecated - fields from the legacy validator registry. + from a ValueError and extract field paths from it. If that fails, it falls + back to simple string matching on the error message using known field paths + from the legacy validator registry. Args: error: The ValueError that may contain a pydantic ValidationError - resource_type: The resource type to get deprecated fields for + resource_type: The resource type to get field paths for Returns: - Tuple of (full field paths, field error details mapping, leaf field names) + Tuple of (full field paths, field error details mapping) """ # Try to extract pydantic ValidationError from the ValueError if hasattr(error, "__cause__") and isinstance( @@ -218,23 +216,23 @@ def extract_deprecated_fields_from_value_error( from orchestrator.core.legacy.registry import LegacyValidatorRegistry error_msg = str(error) - full_field_paths: set[str] = set() + fully_qualified_deprecated_field_paths: set[str] = set() field_errors: dict[str, list[str]] = {} - leaf_field_names: set[str] = set() - # Get all deprecated field names from registered validators for this resource type + # Get all field paths from registered validators for this resource type validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) - deprecated_field_names = { - field for validator in validators for field in validator.deprecated_fields + known_fully_qualified_deprecated_field_paths = { + path + for validator in validators + for path in validator.fully_qualified_deprecated_field_paths } - for field_name in deprecated_field_names: - if field_name in error_msg: - full_field_paths.add(field_name) - leaf_field_names.add(field_name) + for field_path in known_fully_qualified_deprecated_field_paths: + if field_path in error_msg: + fully_qualified_deprecated_field_paths.add(field_path) # For string matching fallback, we don't have detailed error messages - field_errors[field_name] = [ + field_errors[field_path] = [ "Field validation failed (details in error message)" ] - return full_field_paths, field_errors, leaf_field_names + return fully_qualified_deprecated_field_paths, field_errors diff --git a/orchestrator/cli/utils/legacy/list.py b/orchestrator/cli/utils/legacy/list.py index f0a0205c3..c279449f3 100644 --- a/orchestrator/cli/utils/legacy/list.py +++ b/orchestrator/cli/utils/legacy/list.py @@ -48,8 +48,10 @@ def list_legacy_validators(resource_type: CoreResourceKinds) -> None: content_lines.append("") # Deprecated fields - content_lines.append("[bold]Handles deprecated fields:[/bold]") - content_lines.extend(f" • {field}" for field in validator.deprecated_fields) + content_lines.append("[bold]Handles field paths:[/bold]") + content_lines.extend( + f" • {field}" for field in validator.fully_qualified_deprecated_field_paths + ) content_lines.append("") # Version info diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 3eec6a07f..ca4f8b480 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -449,19 +449,20 @@ def _handle_upgrade_validation_error( # Import validators package to trigger registration via __init__.py import orchestrator.core.legacy.validators # noqa: F401 - # Extract field paths, error details, and leaf field names from the error - full_field_paths, field_errors, leaf_field_names = ( + # Extract field paths and error details from the error + fully_qualified_deprecated_field_paths, field_errors = ( extract_deprecated_fields_from_value_error(error, resource_type) ) - # Find applicable legacy validators using leaf field names for better matching + # Find applicable legacy validators using full field paths for precise matching validators = [] - if leaf_field_names: - validators = LegacyValidatorRegistry.find_validators_for_fields( - resource_type=resource_type, field_names=leaf_field_names + if fully_qualified_deprecated_field_paths: + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + resource_type=resource_type, + fully_qualified_deprecated_field_paths=fully_qualified_deprecated_field_paths, ) - # If no validators found by field matching, get all validators for this resource type + # If no validators found by field path matching, get all validators for this resource type if not validators: validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) @@ -473,13 +474,13 @@ def _handle_upgrade_validation_error( "\n[yellow]Some resources could not be loaded due to validation errors.[/yellow]" ) - if full_field_paths: + if fully_qualified_deprecated_field_paths: console.print( - f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(full_field_paths)} field(s)[/yellow]" + f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(fully_qualified_deprecated_field_paths)} field(s)[/yellow]" ) # Show detailed error messages for each field path console.print("\n[bold]Error details:[/bold]") - for field_path in sorted(full_field_paths): + for field_path in sorted(fully_qualified_deprecated_field_paths): console.print(f" • [cyan]{field_path}[/cyan]:") for error_msg in field_errors.get(field_path, []): console.print(f" - {error_msg}") diff --git a/orchestrator/core/legacy/metadata.py b/orchestrator/core/legacy/metadata.py index e74cf9157..e2e7cb40d 100644 --- a/orchestrator/core/legacy/metadata.py +++ b/orchestrator/core/legacy/metadata.py @@ -26,11 +26,6 @@ class LegacyValidatorMetadata(pydantic.BaseModel): pydantic.Field(description="Resource type this validator applies to"), ] - deprecated_fields: Annotated[ - list[str], - pydantic.Field(description="Fields that this validator handles"), - ] - deprecated_from_version: Annotated[ str, pydantic.Field(description="ADO version when these fields were deprecated"), @@ -56,11 +51,10 @@ class LegacyValidatorMetadata(pydantic.BaseModel): ), ] - field_paths: Annotated[ + fully_qualified_deprecated_field_paths: Annotated[ list[str], pydantic.Field( - default_factory=list, - description="Explicit paths to fields (e.g., 'config.properties', 'config.specification.moduleType')", + description="Explicit paths to fields (e.g., 'config.properties', 'config.specification.moduleType')" ), ] diff --git a/orchestrator/core/legacy/registry.py b/orchestrator/core/legacy/registry.py index ae2142aa1..d3778ff2d 100644 --- a/orchestrator/core/legacy/registry.py +++ b/orchestrator/core/legacy/registry.py @@ -52,27 +52,10 @@ def get_validators_for_resource( return [v for v in cls._validators.values() if v.resource_type == resource_type] @classmethod - def find_validators_for_fields( - cls, resource_type: CoreResourceKinds, field_names: set[str] - ) -> list[LegacyValidatorMetadata]: - """Find validators that handle specific deprecated fields - - Args: - resource_type: The resource type to filter by - field_names: Set of field names to search for - - Returns: - List of validator metadata that handle any of the specified fields - """ - return [ - v - for v in cls.get_validators_for_resource(resource_type) - if any(field in v.deprecated_fields for field in field_names) - ] - - @classmethod - def find_validators_for_field_paths( - cls, resource_type: CoreResourceKinds, field_paths: set[str] + def find_validators_for_fully_qualified_deprecated_field_paths( + cls, + resource_type: CoreResourceKinds, + fully_qualified_deprecated_field_paths: set[str], ) -> list[LegacyValidatorMetadata]: """Find validators that handle specific field paths @@ -81,7 +64,7 @@ def find_validators_for_field_paths( Args: resource_type: The resource type to filter by - field_paths: Set of full dotted paths (e.g., 'config.properties') + fully_qualified_deprecated_field_paths: Set of full dotted paths (e.g., 'config.properties') Returns: List of validator metadata that handle any of the specified paths @@ -89,7 +72,10 @@ def find_validators_for_field_paths( return [ v for v in cls.get_validators_for_resource(resource_type) - if any(path in v.field_paths for path in field_paths) + if any( + path in v.fully_qualified_deprecated_field_paths + for path in fully_qualified_deprecated_field_paths + ) ] @classmethod @@ -198,11 +184,10 @@ def resolve_dependencies( def legacy_validator( identifier: str, resource_type: CoreResourceKinds, - deprecated_fields: list[str], + fully_qualified_deprecated_field_paths: list[str], deprecated_from_version: str, removed_from_version: str, description: str, - field_paths: list[str] | None = None, dependencies: list[str] | None = None, ) -> Callable[[Callable[[dict], dict]], Callable[[dict], dict]]: """Decorator to register a legacy validator function @@ -210,11 +195,10 @@ def legacy_validator( Args: identifier: Unique identifier for this validator resource_type: Resource type this validator applies to - deprecated_fields: Fields that this validator handles + fully_qualified_deprecated_field_paths: Explicit paths to fields (e.g., 'config.properties', 'config.specification.moduleType') deprecated_from_version: ADO version when these fields were deprecated removed_from_version: ADO version when automatic upgrade was removed description: Human-readable description of what this validator does - field_paths: Optional explicit paths to fields (e.g., 'config.properties') dependencies: Optional list of validator identifiers that must run before this one Returns: @@ -224,8 +208,7 @@ def legacy_validator( @legacy_validator( identifier="csv_constitutive_columns_migration", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["constitutivePropertyColumns", "propertyMap"], - field_paths=["config.specification.constitutivePropertyColumns"], + fully_qualified_deprecated_field_paths=["config.constitutivePropertyColumns", "config.experiments"], deprecated_from_version="1.3.5", removed_from_version="1.6.0", description="Migrates CSV sample stores from v1 to v2 format", @@ -240,12 +223,11 @@ def decorator(func: Callable[[dict], dict]) -> Callable[[dict], dict]: metadata = LegacyValidatorMetadata( identifier=identifier, resource_type=resource_type, - deprecated_fields=deprecated_fields, deprecated_from_version=deprecated_from_version, removed_from_version=removed_from_version, description=description, validator_function=func, - field_paths=field_paths or [], + fully_qualified_deprecated_field_paths=fully_qualified_deprecated_field_paths, dependencies=dependencies or [], ) LegacyValidatorRegistry.register(metadata) diff --git a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py index c1d48db5a..bbb658e6f 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py @@ -15,11 +15,10 @@ @legacy_validator( identifier="discoveryspace_entitysource_to_samplestore", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["entitySourceIdentifier"], + fully_qualified_deprecated_field_paths=["config.entitySourceIdentifier"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Renames 'entitySourceIdentifier' to 'sampleStoreIdentifier' in discovery space configurations", - field_paths=["config.entitySourceIdentifier"], ) def rename_entitysource_identifier(data: dict) -> dict: """Rename entitySourceIdentifier to sampleStoreIdentifier diff --git a/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py index 54a06ee30..31cd6b4da 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py +++ b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py @@ -11,8 +11,7 @@ @legacy_validator( identifier="discoveryspace_properties_field_removal", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["properties"], - field_paths=["config.properties"], + fully_qualified_deprecated_field_paths=["config.properties"], deprecated_from_version="0.10.1", removed_from_version="1.0.0", description="Removes the deprecated 'properties' field from discovery space configurations", diff --git a/orchestrator/core/legacy/validators/operation/actuators_field_removal.py b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py index 2ec227054..2a1f42b17 100644 --- a/orchestrator/core/legacy/validators/operation/actuators_field_removal.py +++ b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py @@ -11,8 +11,7 @@ @legacy_validator( identifier="operation_actuators_field_removal", resource_type=CoreResourceKinds.OPERATION, - deprecated_fields=["actuators"], - field_paths=["config.actuators"], + fully_qualified_deprecated_field_paths=["config.actuators"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Removes the deprecated 'actuators' field from operation configurations. See https://ibm.github.io/ado/resources/operation/#the-operation-configuration-yaml", diff --git a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py index 92279747b..125cc0157 100644 --- a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py +++ b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py @@ -16,15 +16,14 @@ @legacy_validator( identifier="randomwalk_mode_to_sampler_config", resource_type=CoreResourceKinds.OPERATION, - deprecated_fields=["mode", "grouping", "samplerType"], - deprecated_from_version="1.0.1", - removed_from_version="1.2", - description="Migrates random_walk parameters from flat structure to nested 'samplerConfig'. See https://ibm.github.io/ado/operators/random-walk/#configuring-a-randomwalk", - field_paths=[ + fully_qualified_deprecated_field_paths=[ "config.parameters.mode", "config.parameters.grouping", "config.parameters.samplerType", ], + deprecated_from_version="1.0.1", + removed_from_version="1.2", + description="Migrates random_walk parameters from flat structure to nested 'samplerConfig'. See https://ibm.github.io/ado/operators/random-walk/#configuring-a-randomwalk", ) def migrate_randomwalk_to_sampler_config(data: dict) -> dict: """Migrate random_walk parameters to samplerConfig structure diff --git a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py index 5e7f1dceb..d0e679b25 100644 --- a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py @@ -11,11 +11,10 @@ @legacy_validator( identifier="samplestore_kind_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["kind"], + fully_qualified_deprecated_field_paths=["kind"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts resource kind from 'entitysource' to 'samplestore'", - field_paths=["kind"], ) def migrate_entitysource_kind_to_samplestore(data: dict) -> dict: """Migrate old entitysource kind to samplestore diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index c5f6dfcda..6598cfe2a 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -15,11 +15,10 @@ @legacy_validator( identifier="samplestore_module_type_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["moduleType"], + fully_qualified_deprecated_field_paths=["config.moduleType"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleType value from 'entity_source' to 'sample_store'", - field_paths=["config.moduleType"], ) def migrate_module_type(data: dict) -> dict: """Convert moduleType from entity_source to sample_store @@ -57,11 +56,10 @@ def migrate_module_type(data: dict) -> dict: @legacy_validator( identifier="samplestore_module_class_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["moduleClass"], + fully_qualified_deprecated_field_paths=["config.moduleClass"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleClass values from EntitySource to SampleStore naming (CSVEntitySource -> CSVSampleStore, SQLEntitySource -> SQLSampleStore)", - field_paths=["config.moduleClass"], ) def migrate_module_class(data: dict) -> dict: """Convert moduleClass from EntitySource to SampleStore naming @@ -104,11 +102,10 @@ def migrate_module_class(data: dict) -> dict: @legacy_validator( identifier="samplestore_module_name_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["moduleName"], + fully_qualified_deprecated_field_paths=["config.moduleName"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Updates module paths from entitysource to samplestore (orchestrator.core.entitysource -> orchestrator.core.samplestore)", - field_paths=["config.moduleName"], ) def migrate_module_name(data: dict) -> dict: """Convert moduleName paths from entitysource to samplestore diff --git a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py index 3acab2d04..74b5bec20 100644 --- a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py +++ b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py @@ -11,11 +11,14 @@ @legacy_validator( identifier="csv_constitutive_columns_migration", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["constitutivePropertyColumns", "propertyMap"], + fully_qualified_deprecated_field_paths=[ + "config.constitutivePropertyColumns", + "config.experiments", + ], deprecated_from_version="1.3.5", removed_from_version="1.6.0", description="Migrates CSV sample stores from v1 format (constitutivePropertyColumns in config) to v2 format (per-experiment constitutivePropertyMap)", - field_paths=["config.constitutivePropertyColumns", "config.experiments"], + dependencies=["samplestore_kind_entitysource_to_samplestore"], ) def migrate_csv_v1_to_v2(data: dict) -> dict: """Migrate old CSVSampleStoreDescription format to new format diff --git a/tests/core/test_legacy_registry.py b/tests/core/test_legacy_registry.py index 36060c054..b2c76cb70 100644 --- a/tests/core/test_legacy_registry.py +++ b/tests/core/test_legacy_registry.py @@ -23,7 +23,7 @@ def dummy_validator(data: dict) -> dict: metadata = LegacyValidatorMetadata( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1", "field2"], + fully_qualified_deprecated_field_paths=["config.field1", "config.field2"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -32,7 +32,10 @@ def dummy_validator(data: dict) -> dict: assert metadata.identifier == "test_validator" assert metadata.resource_type == CoreResourceKinds.SAMPLESTORE - assert metadata.deprecated_fields == ["field1", "field2"] + assert metadata.fully_qualified_deprecated_field_paths == [ + "config.field1", + "config.field2", + ] assert metadata.deprecated_from_version == "1.0.0" assert metadata.removed_from_version == "2.0.0" assert metadata.description == "Test validator" @@ -47,7 +50,7 @@ def dummy_validator(data: dict) -> dict: metadata = LegacyValidatorMetadata( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1"], + fully_qualified_deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -79,7 +82,7 @@ def dummy_validator(data: dict) -> dict: metadata = LegacyValidatorMetadata( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1"], + fully_qualified_deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -100,7 +103,7 @@ def dummy_validator(data: dict) -> dict: metadata = LegacyValidatorMetadata( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1"], + fully_qualified_deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -128,7 +131,7 @@ def dummy_validator(data: dict) -> dict: metadata1 = LegacyValidatorMetadata( identifier="samplestore_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1"], + fully_qualified_deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Sample store validator", @@ -138,7 +141,7 @@ def dummy_validator(data: dict) -> dict: metadata2 = LegacyValidatorMetadata( identifier="operation_validator", resource_type=CoreResourceKinds.OPERATION, - deprecated_fields=["field2"], + fully_qualified_deprecated_field_paths=["config.field2"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Operation validator", @@ -162,63 +165,7 @@ def dummy_validator(data: dict) -> dict: assert len(operation_validators) == 1 assert operation_validators[0].identifier == "operation_validator" - def test_find_validators_for_fields(self) -> None: - """Test finding validators that handle specific fields""" - - def dummy_validator(data: dict) -> dict: - return data - - # Register validators with different fields - metadata1 = LegacyValidatorMetadata( - identifier="validator1", - resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1", "field2"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", - description="Validator 1", - validator_function=dummy_validator, - ) - - metadata2 = LegacyValidatorMetadata( - identifier="validator2", - resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field3", "field4"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", - description="Validator 2", - validator_function=dummy_validator, - ) - - LegacyValidatorRegistry.register(metadata1) - LegacyValidatorRegistry.register(metadata2) - - # Find validators for field1 - validators = LegacyValidatorRegistry.find_validators_for_fields( - CoreResourceKinds.SAMPLESTORE, ["field1"] - ) - assert len(validators) == 1 - assert validators[0].identifier == "validator1" - - # Find validators for field3 - validators = LegacyValidatorRegistry.find_validators_for_fields( - CoreResourceKinds.SAMPLESTORE, ["field3"] - ) - assert len(validators) == 1 - assert validators[0].identifier == "validator2" - - # Find validators for multiple fields - validators = LegacyValidatorRegistry.find_validators_for_fields( - CoreResourceKinds.SAMPLESTORE, ["field1", "field3"] - ) - assert len(validators) == 2 - - # Find validators for nonexistent field - validators = LegacyValidatorRegistry.find_validators_for_fields( - CoreResourceKinds.SAMPLESTORE, ["nonexistent"] - ) - assert len(validators) == 0 - - def test_find_validators_for_field_paths(self) -> None: + def test_find_validators_for_fully_qualified_deprecated_field_paths(self) -> None: """Test finding validators that handle specific field paths""" def dummy_validator(data: dict) -> dict: @@ -228,8 +175,7 @@ def dummy_validator(data: dict) -> dict: metadata1 = LegacyValidatorMetadata( identifier="validator1", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1", "field2"], - field_paths=["config.field1", "config.field2"], + fully_qualified_deprecated_field_paths=["config.field1", "config.field2"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 1", @@ -239,8 +185,7 @@ def dummy_validator(data: dict) -> dict: metadata2 = LegacyValidatorMetadata( identifier="validator2", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field3"], - field_paths=["config.specification.field3"], + fully_qualified_deprecated_field_paths=["config.specification.field3"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 2", @@ -250,8 +195,7 @@ def dummy_validator(data: dict) -> dict: metadata3 = LegacyValidatorMetadata( identifier="validator3", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["properties"], - field_paths=["config.properties"], + fully_qualified_deprecated_field_paths=["config.properties"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 3", @@ -263,21 +207,21 @@ def dummy_validator(data: dict) -> dict: LegacyValidatorRegistry.register(metadata3) # Find validators for single full path - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.field1"} ) assert len(validators) == 1 assert validators[0].identifier == "validator1" # Find validators for nested path - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.specification.field3"} ) assert len(validators) == 1 assert validators[0].identifier == "validator2" # Find validators for multiple paths - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.field1", "config.specification.field3"}, ) @@ -286,19 +230,19 @@ def dummy_validator(data: dict) -> dict: assert validator_ids == {"validator1", "validator2"} # Find validators for non-existent path - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.nonexistent"} ) assert len(validators) == 0 # Verify it doesn't match on leaf names alone (more specific than find_validators_for_fields) - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"field1"} # Just leaf name, not full path ) assert len(validators) == 0 # Verify resource type filtering works - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.DISCOVERYSPACE, {"config.properties"} ) assert len(validators) == 1 @@ -313,7 +257,7 @@ def dummy_validator(data: dict) -> dict: metadata1 = LegacyValidatorMetadata( identifier="validator1", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1"], + fully_qualified_deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 1", @@ -323,7 +267,7 @@ def dummy_validator(data: dict) -> dict: metadata2 = LegacyValidatorMetadata( identifier="validator2", resource_type=CoreResourceKinds.OPERATION, - deprecated_fields=["field2"], + fully_qualified_deprecated_field_paths=["config.field2"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 2", @@ -342,7 +286,7 @@ def test_field_path_matching_with_real_validators(self) -> None: import orchestrator.core.legacy.validators # noqa: F401 # Test 1: discoveryspace properties field should match the properties_field_removal validator - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.DISCOVERYSPACE, {"config.properties"} ) assert len(validators) >= 1 @@ -350,7 +294,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "discoveryspace_properties_field_removal" in validator_ids # Test 2: operation actuators field should match the actuators_field_removal validator - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.OPERATION, {"config.actuators"} ) assert len(validators) >= 1 @@ -358,7 +302,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "operation_actuators_field_removal" in validator_ids # Test 3: operation parameters.mode should match randomwalk validator - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.OPERATION, {"config.parameters.mode"} ) assert len(validators) >= 1 @@ -366,7 +310,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "randomwalk_mode_to_sampler_config" in validator_ids # Test 4: samplestore config.moduleType should match the module_type validator - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.moduleType"} ) assert len(validators) >= 1 @@ -374,7 +318,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "samplestore_module_type_entitysource_to_samplestore" in validator_ids # Test 5: samplestore kind field should match the kind validator - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"kind"} ) assert len(validators) >= 1 @@ -382,7 +326,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "samplestore_kind_entitysource_to_samplestore" in validator_ids # Test 6: Multiple paths should return multiple validators - validators = LegacyValidatorRegistry.find_validators_for_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.moduleType", "config.moduleClass", "config.moduleName"}, ) @@ -406,7 +350,7 @@ def test_decorator_registers_validator(self) -> None: @legacy_validator( identifier="test_decorator_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1"], + fully_qualified_deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test decorator validator", @@ -429,7 +373,7 @@ def test_decorator_preserves_function_metadata(self) -> None: @legacy_validator( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["field1"], + fully_qualified_deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -448,7 +392,7 @@ def test_validator_function_execution(self) -> None: @legacy_validator( identifier="transform_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["old_field"], + fully_qualified_deprecated_field_paths=["old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Transform validator", diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index 6fdcb7745..6c4c325a2 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -16,9 +16,28 @@ class TestLegacyValidatorWithPydantic: """Test legacy validators working with pydantic models""" def setup_method(self) -> None: - """Setup method - validators are auto-discovered on import""" - # Don't clear the registry - validators are registered globally on import - # and clearing would break auto-discovery + """Setup method - ensure validators are available before each test""" + # Always import validators to ensure they're registered + # This is safe because Python only executes module-level code once + import orchestrator.core.legacy.validators # noqa: F401 + + # If validators have been cleared by previous tests, we need to restore them + # Since Python won't re-execute the @legacy_validator decorators on re-import, + # we need to save a copy on first access and restore it when needed + if not hasattr(self.__class__, "_initial_validators"): + # First time - save the validators + self.__class__._initial_validators = ( + LegacyValidatorRegistry._validators.copy() + ) + elif ( + not LegacyValidatorRegistry._validators + or "csv_constitutive_columns_migration" + not in LegacyValidatorRegistry._validators + ): + # Validators were cleared - restore them + LegacyValidatorRegistry._validators = ( + self.__class__._initial_validators.copy() + ) def test_validator_applied_during_model_validation(self) -> None: """Test that a legacy validator can be manually applied before pydantic validation""" @@ -31,7 +50,7 @@ class OldModel(pydantic.BaseModel): @legacy_validator( identifier="old_to_new_field", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["old_field"], + fully_qualified_deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Migrate old_field to new_field", @@ -58,12 +77,7 @@ def migrate_old_to_new(data: dict) -> dict: def test_csv_sample_store_migration_validator(self) -> None: """Test the CSV sample store migration validator with realistic data""" - # Import the validator to register it - from orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration import ( # noqa: F401 - migrate_csv_v1_to_v2, - ) - - # Get the validator + # Get the validator (should be registered from setup_method) validator = LegacyValidatorRegistry.get_validator( "csv_constitutive_columns_migration" ) @@ -138,7 +152,7 @@ def test_chained_validators(self) -> None: @legacy_validator( identifier="step1_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["old_field1"], + fully_qualified_deprecated_field_paths=["config.old_field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Step 1 migration", @@ -151,7 +165,7 @@ def step1(data: dict) -> dict: @legacy_validator( identifier="step2_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["intermediate_field"], + fully_qualified_deprecated_field_paths=["config.intermediate_field"], deprecated_from_version="2.0.0", removed_from_version="3.0.0", description="Step 2 migration", @@ -194,7 +208,7 @@ def test_upgrade_handler_applies_legacy_validator(self) -> None: @legacy_validator( identifier="test_upgrade_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["old_field"], + fully_qualified_deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test upgrade validator", @@ -258,7 +272,7 @@ def test_upgrade_handler_validates_validator_resource_type(self) -> None: @legacy_validator( identifier="operation_validator", resource_type=CoreResourceKinds.OPERATION, - deprecated_fields=["old_field"], + fully_qualified_deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Operation validator", @@ -365,7 +379,7 @@ def test_validator_preserves_unrelated_fields(self) -> None: @legacy_validator( identifier="selective_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["old_field"], + fully_qualified_deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Selective validator", @@ -405,7 +419,7 @@ def test_validator_handles_missing_fields_gracefully(self) -> None: @legacy_validator( identifier="graceful_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["optional_old_field"], + fully_qualified_deprecated_field_paths=["config.optional_old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Graceful validator", diff --git a/tests/core/test_upgrade_transaction_safety.py b/tests/core/test_upgrade_transaction_safety.py index ec8a5279e..dc6db38f0 100644 --- a/tests/core/test_upgrade_transaction_safety.py +++ b/tests/core/test_upgrade_transaction_safety.py @@ -26,7 +26,7 @@ def test_all_resources_validated_before_any_saved(self) -> None: @legacy_validator( identifier="test_transaction_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["old_field"], + fully_qualified_deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test transaction validator", @@ -128,7 +128,7 @@ def test_validation_failure_prevents_all_saves(self) -> None: @legacy_validator( identifier="test_failing_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["old_field"], + fully_qualified_deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test failing validator", @@ -226,7 +226,7 @@ def test_empty_resource_list_handled_gracefully(self) -> None: @legacy_validator( identifier="test_empty_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_fields=["old_field"], + fully_qualified_deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test empty validator", diff --git a/tests/core/test_validator_dependencies.py b/tests/core/test_validator_dependencies.py index 24ebe2599..465ee853a 100644 --- a/tests/core/test_validator_dependencies.py +++ b/tests/core/test_validator_dependencies.py @@ -34,12 +34,11 @@ def validator_b(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_a"], + fully_qualified_deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", validator_function=validator_a, - field_paths=["config.field_a"], dependencies=[], ) ) @@ -48,12 +47,11 @@ def validator_b(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_b"], + fully_qualified_deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", validator_function=validator_b, - field_paths=["config.field_b"], dependencies=[], ) ) @@ -87,12 +85,11 @@ def validator_c(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_a"], + fully_qualified_deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", validator_function=validator_a, - field_paths=["config.field_a"], dependencies=[], ) ) @@ -101,12 +98,11 @@ def validator_c(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_b"], + fully_qualified_deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", validator_function=validator_b, - field_paths=["config.field_b"], dependencies=["validator_a"], ) ) @@ -115,12 +111,11 @@ def validator_c(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_c", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_c"], + fully_qualified_deprecated_field_paths=["config.field_c"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator C", validator_function=validator_c, - field_paths=["config.field_c"], dependencies=["validator_b"], ) ) @@ -153,12 +148,11 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_a"], + fully_qualified_deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", validator_function=validator_a, - field_paths=["config.field_a"], dependencies=[], ) ) @@ -167,12 +161,11 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_b"], + fully_qualified_deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", validator_function=validator_b, - field_paths=["config.field_b"], dependencies=["validator_a"], ) ) @@ -181,12 +174,11 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_c", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_c"], + fully_qualified_deprecated_field_paths=["config.field_c"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator C", validator_function=validator_c, - field_paths=["config.field_c"], dependencies=["validator_a"], ) ) @@ -195,12 +187,11 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_d", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_d"], + fully_qualified_deprecated_field_paths=["config.field_d"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator D", validator_function=validator_d, - field_paths=["config.field_d"], dependencies=["validator_b", "validator_c"], ) ) @@ -231,12 +222,11 @@ def validator_b(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_a"], + fully_qualified_deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", validator_function=validator_a, - field_paths=["config.field_a"], dependencies=["validator_b"], ) ) @@ -245,12 +235,11 @@ def validator_b(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_b"], + fully_qualified_deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", validator_function=validator_b, - field_paths=["config.field_b"], dependencies=["validator_a"], ) ) @@ -271,12 +260,11 @@ def validator_a(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_a"], + fully_qualified_deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", validator_function=validator_a, - field_paths=["config.field_a"], dependencies=["nonexistent_validator"], ) ) @@ -309,12 +297,11 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_a"], + fully_qualified_deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", validator_function=validator_a, - field_paths=["config.field_a"], dependencies=[], ) ) @@ -323,12 +310,11 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_b"], + fully_qualified_deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", validator_function=validator_b, - field_paths=["config.field_b"], dependencies=[], ) ) @@ -337,12 +323,11 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_c", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_c"], + fully_qualified_deprecated_field_paths=["config.field_c"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator C", validator_function=validator_c, - field_paths=["config.field_c"], dependencies=["validator_a"], ) ) @@ -351,12 +336,11 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_d", resource_type=CoreResourceKinds.DISCOVERYSPACE, - deprecated_fields=["field_d"], + fully_qualified_deprecated_field_paths=["config.field_d"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator D", validator_function=validator_d, - field_paths=["config.field_d"], dependencies=["validator_b"], ) ) From 419f4118145cfb6c1cfb57db03ecb1f52f44e90a Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 10:46:50 +0000 Subject: [PATCH 28/55] refactor(core): rename field Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 16 +++--- orchestrator/cli/utils/legacy/common.py | 32 +++++------ orchestrator/cli/utils/legacy/list.py | 2 +- orchestrator/cli/utils/resources/handlers.py | 16 +++--- orchestrator/core/legacy/metadata.py | 2 +- orchestrator/core/legacy/registry.py | 19 +++---- .../entitysource_to_samplestore.py | 2 +- .../properties_field_removal.py | 2 +- .../operation/actuators_field_removal.py | 2 +- .../randomwalk_mode_to_sampler_config.py | 2 +- .../resource/entitysource_to_samplestore.py | 2 +- .../samplestore/entitysource_migrations.py | 6 +- .../samplestore/v1_to_v2_csv_migration.py | 2 +- tests/core/test_legacy_registry.py | 56 +++++++++---------- tests/core/test_legacy_validators.py | 14 ++--- tests/core/test_upgrade_transaction_safety.py | 6 +- tests/core/test_validator_dependencies.py | 32 +++++------ 17 files changed, 102 insertions(+), 111 deletions(-) diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index 8eb7f5c55..18f7ee398 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -85,24 +85,24 @@ def handle_validation_error_with_legacy_suggestions( # Import validators package to trigger registration via __init__.py import orchestrator.core.legacy.validators # noqa: F401 from orchestrator.cli.utils.legacy.common import ( - extract_deprecated_fields_from_validation_error, + extract_deprecated_field_paths_from_validation_error, print_validator_suggestions_with_dependencies, ) from orchestrator.core.legacy.registry import LegacyValidatorRegistry # Extract field paths and error details from validation error - fully_qualified_deprecated_field_paths, field_errors = ( - extract_deprecated_fields_from_validation_error(error) + deprecated_field_paths, field_errors = ( + extract_deprecated_field_paths_from_validation_error(error) ) - if not fully_qualified_deprecated_field_paths: + if not deprecated_field_paths: # No fields extracted, show standard error console_print(f"Validation error: {error}", stderr=True) raise typer.Exit(1) from error # Find applicable legacy validators using full field paths for precise matching - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( resource_type=resource_type, - fully_qualified_deprecated_field_paths=fully_qualified_deprecated_field_paths, + deprecated_field_paths=deprecated_field_paths, ) if not validators: @@ -117,11 +117,11 @@ def handle_validation_error_with_legacy_suggestions( f"\n[bold red]Validation Error[/bold red] in {resource_type.value}{resource_id_str}" ) console.print( - f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(fully_qualified_deprecated_field_paths)} field(s)[/yellow]" + f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(deprecated_field_paths)} field(s)[/yellow]" ) # Show detailed error messages for each field path console.print("\n[bold]Error details:[/bold]") - for field_path in sorted(fully_qualified_deprecated_field_paths): + for field_path in sorted(deprecated_field_paths): console.print(f" • [cyan]{field_path}[/cyan]:") for error_msg in field_errors.get(field_path, []): console.print(f" - {error_msg}") diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index 0274ac78e..d45d2c63f 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -36,9 +36,7 @@ def print_validator_suggestions( for validator in validators: console.print(f" • [green]{validator.identifier}[/green]") console.print(f" {validator.description}") - console.print( - f" Handles: {', '.join(validator.fully_qualified_deprecated_field_paths)}" - ) + console.print(f" Handles: {', '.join(validator.deprecated_field_paths)}") console.print(f" Deprecated: v{validator.deprecated_from_version}") console.print() @@ -106,9 +104,7 @@ def print_validator_suggestions_with_dependencies( # Show execution order console.print(f" {i}. [green]{validator.identifier}[/green]") console.print(f" {validator.description}") - console.print( - f" Handles: {', '.join(validator.fully_qualified_deprecated_field_paths)}" - ) + console.print(f" Handles: {', '.join(validator.deprecated_field_paths)}") console.print(f" Deprecated: v{validator.deprecated_from_version}") # Show dependencies if any @@ -152,7 +148,7 @@ def print_validator_suggestions_with_dependencies( # Made with Bob -def extract_deprecated_fields_from_validation_error( +def extract_deprecated_field_paths_from_validation_error( error: pydantic.ValidationError, ) -> tuple[set[str], dict[str, list[str]]]: """Extract field paths and error details from pydantic validation errors @@ -165,14 +161,14 @@ def extract_deprecated_fields_from_validation_error( - full field paths: Set of full dotted paths like 'config.specification.module.moduleType' - field error details: Maps full field path to list of error messages """ - fully_qualified_deprecated_field_paths: set[str] = set() + deprecated_field_paths: set[str] = set() field_errors: dict[str, list[str]] = {} for err in error.errors(): if err.get("loc"): # Build the full dotted path from the location tuple full_path = ".".join(str(loc) for loc in err["loc"]) - fully_qualified_deprecated_field_paths.add(full_path) + deprecated_field_paths.add(full_path) # Store the error message for this field path if full_path not in field_errors: @@ -185,7 +181,7 @@ def extract_deprecated_fields_from_validation_error( field_errors[full_path].append(msg) - return fully_qualified_deprecated_field_paths, field_errors + return deprecated_field_paths, field_errors def extract_deprecated_fields_from_value_error( @@ -210,29 +206,27 @@ def extract_deprecated_fields_from_value_error( if hasattr(error, "__cause__") and isinstance( error.__cause__, pydantic.ValidationError ): - return extract_deprecated_fields_from_validation_error(error.__cause__) + return extract_deprecated_field_paths_from_validation_error(error.__cause__) # Fallback to simple string matching on error message from orchestrator.core.legacy.registry import LegacyValidatorRegistry error_msg = str(error) - fully_qualified_deprecated_field_paths: set[str] = set() + deprecated_field_paths: set[str] = set() field_errors: dict[str, list[str]] = {} # Get all field paths from registered validators for this resource type validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) - known_fully_qualified_deprecated_field_paths = { - path - for validator in validators - for path in validator.fully_qualified_deprecated_field_paths + known_deprecated_field_paths = { + path for validator in validators for path in validator.deprecated_field_paths } - for field_path in known_fully_qualified_deprecated_field_paths: + for field_path in known_deprecated_field_paths: if field_path in error_msg: - fully_qualified_deprecated_field_paths.add(field_path) + deprecated_field_paths.add(field_path) # For string matching fallback, we don't have detailed error messages field_errors[field_path] = [ "Field validation failed (details in error message)" ] - return fully_qualified_deprecated_field_paths, field_errors + return deprecated_field_paths, field_errors diff --git a/orchestrator/cli/utils/legacy/list.py b/orchestrator/cli/utils/legacy/list.py index c279449f3..bb7680770 100644 --- a/orchestrator/cli/utils/legacy/list.py +++ b/orchestrator/cli/utils/legacy/list.py @@ -50,7 +50,7 @@ def list_legacy_validators(resource_type: CoreResourceKinds) -> None: # Deprecated fields content_lines.append("[bold]Handles field paths:[/bold]") content_lines.extend( - f" • {field}" for field in validator.fully_qualified_deprecated_field_paths + f" • {field}" for field in validator.deprecated_field_paths ) content_lines.append("") diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index ca4f8b480..d2a0a0f17 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -450,16 +450,16 @@ def _handle_upgrade_validation_error( import orchestrator.core.legacy.validators # noqa: F401 # Extract field paths and error details from the error - fully_qualified_deprecated_field_paths, field_errors = ( - extract_deprecated_fields_from_value_error(error, resource_type) + deprecated_field_paths, field_errors = extract_deprecated_fields_from_value_error( + error, resource_type ) # Find applicable legacy validators using full field paths for precise matching validators = [] - if fully_qualified_deprecated_field_paths: - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + if deprecated_field_paths: + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( resource_type=resource_type, - fully_qualified_deprecated_field_paths=fully_qualified_deprecated_field_paths, + deprecated_field_paths=deprecated_field_paths, ) # If no validators found by field path matching, get all validators for this resource type @@ -474,13 +474,13 @@ def _handle_upgrade_validation_error( "\n[yellow]Some resources could not be loaded due to validation errors.[/yellow]" ) - if fully_qualified_deprecated_field_paths: + if deprecated_field_paths: console.print( - f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(fully_qualified_deprecated_field_paths)} field(s)[/yellow]" + f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(deprecated_field_paths)} field(s)[/yellow]" ) # Show detailed error messages for each field path console.print("\n[bold]Error details:[/bold]") - for field_path in sorted(fully_qualified_deprecated_field_paths): + for field_path in sorted(deprecated_field_paths): console.print(f" • [cyan]{field_path}[/cyan]:") for error_msg in field_errors.get(field_path, []): console.print(f" - {error_msg}") diff --git a/orchestrator/core/legacy/metadata.py b/orchestrator/core/legacy/metadata.py index e2e7cb40d..f9980e632 100644 --- a/orchestrator/core/legacy/metadata.py +++ b/orchestrator/core/legacy/metadata.py @@ -51,7 +51,7 @@ class LegacyValidatorMetadata(pydantic.BaseModel): ), ] - fully_qualified_deprecated_field_paths: Annotated[ + deprecated_field_paths: Annotated[ list[str], pydantic.Field( description="Explicit paths to fields (e.g., 'config.properties', 'config.specification.moduleType')" diff --git a/orchestrator/core/legacy/registry.py b/orchestrator/core/legacy/registry.py index d3778ff2d..66745f6e7 100644 --- a/orchestrator/core/legacy/registry.py +++ b/orchestrator/core/legacy/registry.py @@ -52,10 +52,10 @@ def get_validators_for_resource( return [v for v in cls._validators.values() if v.resource_type == resource_type] @classmethod - def find_validators_for_fully_qualified_deprecated_field_paths( + def find_validators_for_deprecated_field_paths( cls, resource_type: CoreResourceKinds, - fully_qualified_deprecated_field_paths: set[str], + deprecated_field_paths: set[str], ) -> list[LegacyValidatorMetadata]: """Find validators that handle specific field paths @@ -64,7 +64,7 @@ def find_validators_for_fully_qualified_deprecated_field_paths( Args: resource_type: The resource type to filter by - fully_qualified_deprecated_field_paths: Set of full dotted paths (e.g., 'config.properties') + deprecated_field_paths: Set of full dotted paths (e.g., 'config.properties') Returns: List of validator metadata that handle any of the specified paths @@ -72,10 +72,7 @@ def find_validators_for_fully_qualified_deprecated_field_paths( return [ v for v in cls.get_validators_for_resource(resource_type) - if any( - path in v.fully_qualified_deprecated_field_paths - for path in fully_qualified_deprecated_field_paths - ) + if any(path in v.deprecated_field_paths for path in deprecated_field_paths) ] @classmethod @@ -184,7 +181,7 @@ def resolve_dependencies( def legacy_validator( identifier: str, resource_type: CoreResourceKinds, - fully_qualified_deprecated_field_paths: list[str], + deprecated_field_paths: list[str], deprecated_from_version: str, removed_from_version: str, description: str, @@ -195,7 +192,7 @@ def legacy_validator( Args: identifier: Unique identifier for this validator resource_type: Resource type this validator applies to - fully_qualified_deprecated_field_paths: Explicit paths to fields (e.g., 'config.properties', 'config.specification.moduleType') + deprecated_field_paths: Explicit paths to fields (e.g., 'config.properties', 'config.specification.moduleType') deprecated_from_version: ADO version when these fields were deprecated removed_from_version: ADO version when automatic upgrade was removed description: Human-readable description of what this validator does @@ -208,7 +205,7 @@ def legacy_validator( @legacy_validator( identifier="csv_constitutive_columns_migration", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.constitutivePropertyColumns", "config.experiments"], + deprecated_field_paths=["config.constitutivePropertyColumns", "config.experiments"], deprecated_from_version="1.3.5", removed_from_version="1.6.0", description="Migrates CSV sample stores from v1 to v2 format", @@ -227,7 +224,7 @@ def decorator(func: Callable[[dict], dict]) -> Callable[[dict], dict]: removed_from_version=removed_from_version, description=description, validator_function=func, - fully_qualified_deprecated_field_paths=fully_qualified_deprecated_field_paths, + deprecated_field_paths=deprecated_field_paths, dependencies=dependencies or [], ) LegacyValidatorRegistry.register(metadata) diff --git a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py index bbb658e6f..583d43ee9 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py @@ -15,7 +15,7 @@ @legacy_validator( identifier="discoveryspace_entitysource_to_samplestore", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.entitySourceIdentifier"], + deprecated_field_paths=["config.entitySourceIdentifier"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Renames 'entitySourceIdentifier' to 'sampleStoreIdentifier' in discovery space configurations", diff --git a/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py index 31cd6b4da..0e271c578 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py +++ b/orchestrator/core/legacy/validators/discoveryspace/properties_field_removal.py @@ -11,7 +11,7 @@ @legacy_validator( identifier="discoveryspace_properties_field_removal", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.properties"], + deprecated_field_paths=["config.properties"], deprecated_from_version="0.10.1", removed_from_version="1.0.0", description="Removes the deprecated 'properties' field from discovery space configurations", diff --git a/orchestrator/core/legacy/validators/operation/actuators_field_removal.py b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py index 2a1f42b17..d54698ecf 100644 --- a/orchestrator/core/legacy/validators/operation/actuators_field_removal.py +++ b/orchestrator/core/legacy/validators/operation/actuators_field_removal.py @@ -11,7 +11,7 @@ @legacy_validator( identifier="operation_actuators_field_removal", resource_type=CoreResourceKinds.OPERATION, - fully_qualified_deprecated_field_paths=["config.actuators"], + deprecated_field_paths=["config.actuators"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Removes the deprecated 'actuators' field from operation configurations. See https://ibm.github.io/ado/resources/operation/#the-operation-configuration-yaml", diff --git a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py index 125cc0157..ee00d3307 100644 --- a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py +++ b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py @@ -16,7 +16,7 @@ @legacy_validator( identifier="randomwalk_mode_to_sampler_config", resource_type=CoreResourceKinds.OPERATION, - fully_qualified_deprecated_field_paths=[ + deprecated_field_paths=[ "config.parameters.mode", "config.parameters.grouping", "config.parameters.samplerType", diff --git a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py index d0e679b25..737db2137 100644 --- a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py @@ -11,7 +11,7 @@ @legacy_validator( identifier="samplestore_kind_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["kind"], + deprecated_field_paths=["kind"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts resource kind from 'entitysource' to 'samplestore'", diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index 6598cfe2a..8e6ebdb2c 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -15,7 +15,7 @@ @legacy_validator( identifier="samplestore_module_type_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.moduleType"], + deprecated_field_paths=["config.moduleType"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleType value from 'entity_source' to 'sample_store'", @@ -56,7 +56,7 @@ def migrate_module_type(data: dict) -> dict: @legacy_validator( identifier="samplestore_module_class_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.moduleClass"], + deprecated_field_paths=["config.moduleClass"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleClass values from EntitySource to SampleStore naming (CSVEntitySource -> CSVSampleStore, SQLEntitySource -> SQLSampleStore)", @@ -102,7 +102,7 @@ def migrate_module_class(data: dict) -> dict: @legacy_validator( identifier="samplestore_module_name_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.moduleName"], + deprecated_field_paths=["config.moduleName"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Updates module paths from entitysource to samplestore (orchestrator.core.entitysource -> orchestrator.core.samplestore)", diff --git a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py index 74b5bec20..2bec84a2b 100644 --- a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py +++ b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py @@ -11,7 +11,7 @@ @legacy_validator( identifier="csv_constitutive_columns_migration", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=[ + deprecated_field_paths=[ "config.constitutivePropertyColumns", "config.experiments", ], diff --git a/tests/core/test_legacy_registry.py b/tests/core/test_legacy_registry.py index b2c76cb70..fdc6ad7f6 100644 --- a/tests/core/test_legacy_registry.py +++ b/tests/core/test_legacy_registry.py @@ -23,7 +23,7 @@ def dummy_validator(data: dict) -> dict: metadata = LegacyValidatorMetadata( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.field1", "config.field2"], + deprecated_field_paths=["config.field1", "config.field2"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -32,7 +32,7 @@ def dummy_validator(data: dict) -> dict: assert metadata.identifier == "test_validator" assert metadata.resource_type == CoreResourceKinds.SAMPLESTORE - assert metadata.fully_qualified_deprecated_field_paths == [ + assert metadata.deprecated_field_paths == [ "config.field1", "config.field2", ] @@ -50,7 +50,7 @@ def dummy_validator(data: dict) -> dict: metadata = LegacyValidatorMetadata( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.field1"], + deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -82,7 +82,7 @@ def dummy_validator(data: dict) -> dict: metadata = LegacyValidatorMetadata( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.field1"], + deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -103,7 +103,7 @@ def dummy_validator(data: dict) -> dict: metadata = LegacyValidatorMetadata( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.field1"], + deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -131,7 +131,7 @@ def dummy_validator(data: dict) -> dict: metadata1 = LegacyValidatorMetadata( identifier="samplestore_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.field1"], + deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Sample store validator", @@ -141,7 +141,7 @@ def dummy_validator(data: dict) -> dict: metadata2 = LegacyValidatorMetadata( identifier="operation_validator", resource_type=CoreResourceKinds.OPERATION, - fully_qualified_deprecated_field_paths=["config.field2"], + deprecated_field_paths=["config.field2"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Operation validator", @@ -165,7 +165,7 @@ def dummy_validator(data: dict) -> dict: assert len(operation_validators) == 1 assert operation_validators[0].identifier == "operation_validator" - def test_find_validators_for_fully_qualified_deprecated_field_paths(self) -> None: + def test_find_validators_for_deprecated_field_paths(self) -> None: """Test finding validators that handle specific field paths""" def dummy_validator(data: dict) -> dict: @@ -175,7 +175,7 @@ def dummy_validator(data: dict) -> dict: metadata1 = LegacyValidatorMetadata( identifier="validator1", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.field1", "config.field2"], + deprecated_field_paths=["config.field1", "config.field2"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 1", @@ -185,7 +185,7 @@ def dummy_validator(data: dict) -> dict: metadata2 = LegacyValidatorMetadata( identifier="validator2", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.specification.field3"], + deprecated_field_paths=["config.specification.field3"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 2", @@ -195,7 +195,7 @@ def dummy_validator(data: dict) -> dict: metadata3 = LegacyValidatorMetadata( identifier="validator3", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.properties"], + deprecated_field_paths=["config.properties"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 3", @@ -207,21 +207,21 @@ def dummy_validator(data: dict) -> dict: LegacyValidatorRegistry.register(metadata3) # Find validators for single full path - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.field1"} ) assert len(validators) == 1 assert validators[0].identifier == "validator1" # Find validators for nested path - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.specification.field3"} ) assert len(validators) == 1 assert validators[0].identifier == "validator2" # Find validators for multiple paths - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.field1", "config.specification.field3"}, ) @@ -230,19 +230,19 @@ def dummy_validator(data: dict) -> dict: assert validator_ids == {"validator1", "validator2"} # Find validators for non-existent path - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.nonexistent"} ) assert len(validators) == 0 # Verify it doesn't match on leaf names alone (more specific than find_validators_for_fields) - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"field1"} # Just leaf name, not full path ) assert len(validators) == 0 # Verify resource type filtering works - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.DISCOVERYSPACE, {"config.properties"} ) assert len(validators) == 1 @@ -257,7 +257,7 @@ def dummy_validator(data: dict) -> dict: metadata1 = LegacyValidatorMetadata( identifier="validator1", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.field1"], + deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 1", @@ -267,7 +267,7 @@ def dummy_validator(data: dict) -> dict: metadata2 = LegacyValidatorMetadata( identifier="validator2", resource_type=CoreResourceKinds.OPERATION, - fully_qualified_deprecated_field_paths=["config.field2"], + deprecated_field_paths=["config.field2"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator 2", @@ -286,7 +286,7 @@ def test_field_path_matching_with_real_validators(self) -> None: import orchestrator.core.legacy.validators # noqa: F401 # Test 1: discoveryspace properties field should match the properties_field_removal validator - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.DISCOVERYSPACE, {"config.properties"} ) assert len(validators) >= 1 @@ -294,7 +294,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "discoveryspace_properties_field_removal" in validator_ids # Test 2: operation actuators field should match the actuators_field_removal validator - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.OPERATION, {"config.actuators"} ) assert len(validators) >= 1 @@ -302,7 +302,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "operation_actuators_field_removal" in validator_ids # Test 3: operation parameters.mode should match randomwalk validator - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.OPERATION, {"config.parameters.mode"} ) assert len(validators) >= 1 @@ -310,7 +310,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "randomwalk_mode_to_sampler_config" in validator_ids # Test 4: samplestore config.moduleType should match the module_type validator - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.moduleType"} ) assert len(validators) >= 1 @@ -318,7 +318,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "samplestore_module_type_entitysource_to_samplestore" in validator_ids # Test 5: samplestore kind field should match the kind validator - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"kind"} ) assert len(validators) >= 1 @@ -326,7 +326,7 @@ def test_field_path_matching_with_real_validators(self) -> None: assert "samplestore_kind_entitysource_to_samplestore" in validator_ids # Test 6: Multiple paths should return multiple validators - validators = LegacyValidatorRegistry.find_validators_for_fully_qualified_deprecated_field_paths( + validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, {"config.moduleType", "config.moduleClass", "config.moduleName"}, ) @@ -350,7 +350,7 @@ def test_decorator_registers_validator(self) -> None: @legacy_validator( identifier="test_decorator_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.field1"], + deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test decorator validator", @@ -373,7 +373,7 @@ def test_decorator_preserves_function_metadata(self) -> None: @legacy_validator( identifier="test_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.field1"], + deprecated_field_paths=["config.field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test validator", @@ -392,7 +392,7 @@ def test_validator_function_execution(self) -> None: @legacy_validator( identifier="transform_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["old_field"], + deprecated_field_paths=["old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Transform validator", diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index 6c4c325a2..9157bf066 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -50,7 +50,7 @@ class OldModel(pydantic.BaseModel): @legacy_validator( identifier="old_to_new_field", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Migrate old_field to new_field", @@ -152,7 +152,7 @@ def test_chained_validators(self) -> None: @legacy_validator( identifier="step1_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.old_field1"], + deprecated_field_paths=["config.old_field1"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Step 1 migration", @@ -165,7 +165,7 @@ def step1(data: dict) -> dict: @legacy_validator( identifier="step2_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.intermediate_field"], + deprecated_field_paths=["config.intermediate_field"], deprecated_from_version="2.0.0", removed_from_version="3.0.0", description="Step 2 migration", @@ -208,7 +208,7 @@ def test_upgrade_handler_applies_legacy_validator(self) -> None: @legacy_validator( identifier="test_upgrade_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test upgrade validator", @@ -272,7 +272,7 @@ def test_upgrade_handler_validates_validator_resource_type(self) -> None: @legacy_validator( identifier="operation_validator", resource_type=CoreResourceKinds.OPERATION, - fully_qualified_deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Operation validator", @@ -379,7 +379,7 @@ def test_validator_preserves_unrelated_fields(self) -> None: @legacy_validator( identifier="selective_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Selective validator", @@ -419,7 +419,7 @@ def test_validator_handles_missing_fields_gracefully(self) -> None: @legacy_validator( identifier="graceful_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.optional_old_field"], + deprecated_field_paths=["config.optional_old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Graceful validator", diff --git a/tests/core/test_upgrade_transaction_safety.py b/tests/core/test_upgrade_transaction_safety.py index dc6db38f0..8fbeedd50 100644 --- a/tests/core/test_upgrade_transaction_safety.py +++ b/tests/core/test_upgrade_transaction_safety.py @@ -26,7 +26,7 @@ def test_all_resources_validated_before_any_saved(self) -> None: @legacy_validator( identifier="test_transaction_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test transaction validator", @@ -128,7 +128,7 @@ def test_validation_failure_prevents_all_saves(self) -> None: @legacy_validator( identifier="test_failing_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test failing validator", @@ -226,7 +226,7 @@ def test_empty_resource_list_handled_gracefully(self) -> None: @legacy_validator( identifier="test_empty_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - fully_qualified_deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test empty validator", diff --git a/tests/core/test_validator_dependencies.py b/tests/core/test_validator_dependencies.py index 465ee853a..4becfeaa4 100644 --- a/tests/core/test_validator_dependencies.py +++ b/tests/core/test_validator_dependencies.py @@ -34,7 +34,7 @@ def validator_b(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_a"], + deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", @@ -47,7 +47,7 @@ def validator_b(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_b"], + deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", @@ -85,7 +85,7 @@ def validator_c(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_a"], + deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", @@ -98,7 +98,7 @@ def validator_c(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_b"], + deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", @@ -111,7 +111,7 @@ def validator_c(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_c", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_c"], + deprecated_field_paths=["config.field_c"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator C", @@ -148,7 +148,7 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_a"], + deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", @@ -161,7 +161,7 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_b"], + deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", @@ -174,7 +174,7 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_c", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_c"], + deprecated_field_paths=["config.field_c"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator C", @@ -187,7 +187,7 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_d", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_d"], + deprecated_field_paths=["config.field_d"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator D", @@ -222,7 +222,7 @@ def validator_b(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_a"], + deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", @@ -235,7 +235,7 @@ def validator_b(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_b"], + deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", @@ -260,7 +260,7 @@ def validator_a(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_a"], + deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", @@ -297,7 +297,7 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_a", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_a"], + deprecated_field_paths=["config.field_a"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator A", @@ -310,7 +310,7 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_b", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_b"], + deprecated_field_paths=["config.field_b"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator B", @@ -323,7 +323,7 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_c", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_c"], + deprecated_field_paths=["config.field_c"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator C", @@ -336,7 +336,7 @@ def validator_d(data: dict) -> dict: LegacyValidatorMetadata( identifier="validator_d", resource_type=CoreResourceKinds.DISCOVERYSPACE, - fully_qualified_deprecated_field_paths=["config.field_d"], + deprecated_field_paths=["config.field_d"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Validator D", From 2709eb6e6a501689fc40393d5c4223a7b59039a8 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 11:03:14 +0000 Subject: [PATCH 29/55] refactor(cli): remove unused function Signed-off-by: Alessandro Pomponio --- orchestrator/cli/exceptions/handlers.py | 73 ------------------------- 1 file changed, 73 deletions(-) diff --git a/orchestrator/cli/exceptions/handlers.py b/orchestrator/cli/exceptions/handlers.py index 18f7ee398..6806944ff 100644 --- a/orchestrator/cli/exceptions/handlers.py +++ b/orchestrator/cli/exceptions/handlers.py @@ -3,9 +3,7 @@ from typing import NoReturn -import pydantic import typer -from rich.console import Console from orchestrator.cli.utils.output.prints import ( console_print, @@ -14,7 +12,6 @@ no_resource_with_id_in_db_error_str, unknown_experiment_error_str, ) -from orchestrator.core.resources import CoreResourceKinds from orchestrator.metastore.base import ( DeleteFromDatabaseError, NoRelatedResourcesError, @@ -65,73 +62,3 @@ def handle_resource_deletion_error(error: DeleteFromDatabaseError) -> NoReturn: stderr=True, ) raise typer.Exit(1) from error - - -def handle_validation_error_with_legacy_suggestions( - error: pydantic.ValidationError, - resource_type: CoreResourceKinds, - resource_identifier: str | None = None, -) -> NoReturn: - """Handle pydantic validation errors and suggest legacy validators if applicable - - Args: - error: The pydantic validation error - resource_type: The type of resource that failed validation - resource_identifier: Optional identifier of the resource - - Raises: - typer.Exit: Always exits with code 1 - """ - # Import validators package to trigger registration via __init__.py - import orchestrator.core.legacy.validators # noqa: F401 - from orchestrator.cli.utils.legacy.common import ( - extract_deprecated_field_paths_from_validation_error, - print_validator_suggestions_with_dependencies, - ) - from orchestrator.core.legacy.registry import LegacyValidatorRegistry - - # Extract field paths and error details from validation error - deprecated_field_paths, field_errors = ( - extract_deprecated_field_paths_from_validation_error(error) - ) - if not deprecated_field_paths: - # No fields extracted, show standard error - console_print(f"Validation error: {error}", stderr=True) - raise typer.Exit(1) from error - - # Find applicable legacy validators using full field paths for precise matching - validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( - resource_type=resource_type, - deprecated_field_paths=deprecated_field_paths, - ) - - if not validators: - # No legacy validators available, show standard error - console_print(f"Validation error: {error}", stderr=True) - raise typer.Exit(1) from error - - # Display helpful error message with suggestions - console = Console(stderr=True) - resource_id_str = f" '{resource_identifier}'" if resource_identifier else "" - console.print( - f"\n[bold red]Validation Error[/bold red] in {resource_type.value}{resource_id_str}" - ) - console.print( - f"\n[bold]Fields with validation errors:[/bold] [yellow]{len(deprecated_field_paths)} field(s)[/yellow]" - ) - # Show detailed error messages for each field path - console.print("\n[bold]Error details:[/bold]") - for field_path in sorted(deprecated_field_paths): - console.print(f" • [cyan]{field_path}[/cyan]:") - for error_msg in field_errors.get(field_path, []): - console.print(f" - {error_msg}") - console.print() - - # Use enhanced suggestion printer with dependency information - print_validator_suggestions_with_dependencies( - validators=validators, - resource_type=resource_type, - console=console, - ) - - raise typer.Exit(1) from error From 19fa9a98951282f8ff4566a328b187ea9c486520 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 11:09:36 +0000 Subject: [PATCH 30/55] refactor(cli): remove print_validator_suggestions Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/legacy/common.py | 40 -------------------- orchestrator/cli/utils/resources/handlers.py | 5 +-- 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index d45d2c63f..a762b2622 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -13,46 +13,6 @@ from orchestrator.core.resources import CoreResourceKinds -def print_validator_suggestions( - validators: list["LegacyValidatorMetadata"], - resource_type: "CoreResourceKinds", - console: Console, - show_all_validators: bool = False, -) -> None: - """Print legacy validator suggestions to the console - - Args: - validators: List of applicable validators - resource_type: The resource type - console: Rich console to print to - show_all_validators: If True, show all validators in the command example - """ - # Resources can be referenced by their CoreResourceKinds value or by shorthands - # from cli_shorthands_to_cli_names in orchestrator/cli/utils/resources/mappings.py - resource_cli_name = resource_type.value - - console.print("\n[bold cyan]Available legacy validators:[/bold cyan]\n") - - for validator in validators: - console.print(f" • [green]{validator.identifier}[/green]") - console.print(f" {validator.description}") - console.print(f" Handles: {', '.join(validator.deprecated_field_paths)}") - console.print(f" Deprecated: v{validator.deprecated_from_version}") - console.print() - - console.print("[bold magenta]To upgrade using legacy validators:[/bold magenta]") - if show_all_validators: - validator_args = " ".join( - f"--apply-legacy-validator {v.identifier}" for v in validators - ) - else: - validator_args = f"--apply-legacy-validator {validators[0].identifier}" - console.print(f" ado upgrade {resource_cli_name} {validator_args}") - console.print() - console.print("[bold magenta]To list all legacy validators:[/bold magenta]") - console.print(f" ado upgrade {resource_cli_name} --list-legacy-validators") - - def print_validator_suggestions_with_dependencies( validators: list["LegacyValidatorMetadata"], resource_type: "CoreResourceKinds", diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index d2a0a0f17..9b86d6245 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -440,7 +440,7 @@ def _handle_upgrade_validation_error( from orchestrator.cli.utils.legacy.common import ( extract_deprecated_fields_from_value_error, - print_validator_suggestions, + print_validator_suggestions_with_dependencies, ) from orchestrator.core.legacy.registry import LegacyValidatorRegistry @@ -486,11 +486,10 @@ def _handle_upgrade_validation_error( console.print(f" - {error_msg}") if validators: - print_validator_suggestions( + print_validator_suggestions_with_dependencies( validators=validators, resource_type=resource_type, console=console, - show_all_validators=True, ) else: console.print( From 189fee233283dc316378c7a20624b3ca54213e4f Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 11:15:17 +0000 Subject: [PATCH 31/55] refactor(cli): consolidate duplicate functions into one Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/legacy/common.py | 144 ++++++++++--------- orchestrator/cli/utils/resources/handlers.py | 4 +- 2 files changed, 77 insertions(+), 71 deletions(-) diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index a762b2622..94b7dcb6b 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -108,85 +108,91 @@ def print_validator_suggestions_with_dependencies( # Made with Bob -def extract_deprecated_field_paths_from_validation_error( - error: pydantic.ValidationError, +def extract_deprecated_field_paths( + error: pydantic.ValidationError | ValueError, + resource_type: "CoreResourceKinds | None" = None, ) -> tuple[set[str], dict[str, list[str]]]: - """Extract field paths and error details from pydantic validation errors + """Extract field paths and error details from validation errors + + This function handles both pydantic ValidationError and ValueError types. + For ValueError, it attempts to extract an underlying pydantic ValidationError + from the error's __cause__. If that fails, it falls back to simple string + matching on the error message using known field paths from the legacy + validator registry (requires resource_type parameter). Args: - error: The pydantic validation error + error: The validation error (pydantic.ValidationError or ValueError) + resource_type: The resource type to get field paths for (required for + ValueError fallback to string matching) Returns: Tuple of (full field paths, field error details mapping) - full field paths: Set of full dotted paths like 'config.specification.module.moduleType' - field error details: Maps full field path to list of error messages - """ - deprecated_field_paths: set[str] = set() - field_errors: dict[str, list[str]] = {} - - for err in error.errors(): - if err.get("loc"): - # Build the full dotted path from the location tuple - full_path = ".".join(str(loc) for loc in err["loc"]) - deprecated_field_paths.add(full_path) - - # Store the error message for this field path - if full_path not in field_errors: - field_errors[full_path] = [] - # Build a descriptive error message - msg = err.get("msg", "") - if err.get("input"): - msg = f"{msg} (got: {err['input']})" - - field_errors[full_path].append(msg) - - return deprecated_field_paths, field_errors - - -def extract_deprecated_fields_from_value_error( - error: ValueError, - resource_type: "CoreResourceKinds", -) -> tuple[set[str], dict[str, list[str]]]: - """Extract field paths from ValueError containing pydantic validation errors - - This function attempts to extract the underlying pydantic ValidationError - from a ValueError and extract field paths from it. If that fails, it falls - back to simple string matching on the error message using known field paths - from the legacy validator registry. - - Args: - error: The ValueError that may contain a pydantic ValidationError - resource_type: The resource type to get field paths for - - Returns: - Tuple of (full field paths, field error details mapping) + Raises: + ValueError: If error is a ValueError without a ValidationError cause and + resource_type is not provided """ - # Try to extract pydantic ValidationError from the ValueError - if hasattr(error, "__cause__") and isinstance( - error.__cause__, pydantic.ValidationError - ): - return extract_deprecated_field_paths_from_validation_error(error.__cause__) - - # Fallback to simple string matching on error message - from orchestrator.core.legacy.registry import LegacyValidatorRegistry - - error_msg = str(error) deprecated_field_paths: set[str] = set() field_errors: dict[str, list[str]] = {} - # Get all field paths from registered validators for this resource type - validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) - known_deprecated_field_paths = { - path for validator in validators for path in validator.deprecated_field_paths - } - - for field_path in known_deprecated_field_paths: - if field_path in error_msg: - deprecated_field_paths.add(field_path) - # For string matching fallback, we don't have detailed error messages - field_errors[field_path] = [ - "Field validation failed (details in error message)" - ] - - return deprecated_field_paths, field_errors + # Handle pydantic ValidationError directly + if isinstance(error, pydantic.ValidationError): + for err in error.errors(): + if err.get("loc"): + # Build the full dotted path from the location tuple + full_path = ".".join(str(loc) for loc in err["loc"]) + deprecated_field_paths.add(full_path) + + # Store the error message for this field path + if full_path not in field_errors: + field_errors[full_path] = [] + + # Build a descriptive error message + msg = err.get("msg", "") + if err.get("input"): + msg = f"{msg} (got: {err['input']})" + + field_errors[full_path].append(msg) + + return deprecated_field_paths, field_errors + + # Handle ValueError - try to extract pydantic ValidationError from __cause__ + if isinstance(error, ValueError): + if hasattr(error, "__cause__") and isinstance( + error.__cause__, pydantic.ValidationError + ): + # Recursively handle the underlying ValidationError + return extract_deprecated_field_paths(error.__cause__, resource_type) + + # Fallback to simple string matching on error message + if resource_type is None: + raise ValueError( + "resource_type is required for ValueError without ValidationError cause" + ) + + from orchestrator.core.legacy.registry import LegacyValidatorRegistry + + error_msg = str(error) + + # Get all field paths from registered validators for this resource type + validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) + known_deprecated_field_paths = { + path + for validator in validators + for path in validator.deprecated_field_paths + } + + for field_path in known_deprecated_field_paths: + if field_path in error_msg: + deprecated_field_paths.add(field_path) + # For string matching fallback, we don't have detailed error messages + field_errors[field_path] = [ + "Field validation failed (details in error message)" + ] + + return deprecated_field_paths, field_errors + + # Should not reach here due to type hints, but handle gracefully + raise TypeError(f"Unsupported error type: {type(error)}") diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 9b86d6245..39ac719ec 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -439,7 +439,7 @@ def _handle_upgrade_validation_error( from rich.console import Console from orchestrator.cli.utils.legacy.common import ( - extract_deprecated_fields_from_value_error, + extract_deprecated_field_paths, print_validator_suggestions_with_dependencies, ) from orchestrator.core.legacy.registry import LegacyValidatorRegistry @@ -450,7 +450,7 @@ def _handle_upgrade_validation_error( import orchestrator.core.legacy.validators # noqa: F401 # Extract field paths and error details from the error - deprecated_field_paths, field_errors = extract_deprecated_fields_from_value_error( + deprecated_field_paths, field_errors = extract_deprecated_field_paths( error, resource_type ) From 1c44854423deafda9437f210938ec2c00ec8d75f Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 11:49:46 +0000 Subject: [PATCH 32/55] refactor: style improvements Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/legacy/common.py | 198 +++++++++---------- orchestrator/cli/utils/resources/handlers.py | 1 + 2 files changed, 99 insertions(+), 100 deletions(-) diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index 94b7dcb6b..3e806cffe 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -8,106 +8,20 @@ import pydantic from rich.console import Console +from orchestrator.cli.utils.output.prints import ( + ERROR, + HINT, + INFO, + WARN, + console_print, + cyan, +) + if TYPE_CHECKING: from orchestrator.core.legacy.metadata import LegacyValidatorMetadata from orchestrator.core.resources import CoreResourceKinds -def print_validator_suggestions_with_dependencies( - validators: list["LegacyValidatorMetadata"], - resource_type: "CoreResourceKinds", - console: Console, -) -> None: - """Print legacy validator suggestions with dependency information - - This enhanced version resolves dependencies and shows validators in the - correct execution order, along with dependency information. - - Args: - validators: List of applicable validators - resource_type: The resource type - console: Rich console to print to - """ - from orchestrator.core.legacy.registry import LegacyValidatorRegistry - - # Resources can be referenced by their CoreResourceKinds value or by shorthands - resource_cli_name = resource_type.value - - # Get validator identifiers - validator_ids = [v.identifier for v in validators] - - # Resolve dependencies to get correct order - try: - ordered_ids, missing_deps = LegacyValidatorRegistry.resolve_dependencies( - validator_ids - ) - except ValueError as e: - # Circular dependency detected - console.print( - f"\n[bold red]Warning:[/bold red] {e}", - style="red", - ) - # Fall back to original order - ordered_ids = validator_ids - missing_deps = [] - - # Get ordered validators (filter out None values) - ordered_validators: list[LegacyValidatorMetadata] = [] - for vid in ordered_ids: - validator = LegacyValidatorRegistry.get_validator(vid) - if validator is not None: - ordered_validators.append(validator) - - console.print("\n[bold cyan]Available legacy validators:[/bold cyan]\n") - - for i, validator in enumerate(ordered_validators, 1): - # Show execution order - console.print(f" {i}. [green]{validator.identifier}[/green]") - console.print(f" {validator.description}") - console.print(f" Handles: {', '.join(validator.deprecated_field_paths)}") - console.print(f" Deprecated: v{validator.deprecated_from_version}") - - # Show dependencies if any - if validator.dependencies: - dep_names = [] - for dep_id in validator.dependencies: - dep_validator = LegacyValidatorRegistry.get_validator(dep_id) - if dep_validator: - dep_names.append(dep_validator.identifier) - else: - dep_names.append(f"{dep_id} [red](missing)[/red]") - console.print(f" Dependencies: {', '.join(dep_names)}") - - console.print() - - # Warn about missing dependencies - if missing_deps: - console.print( - f"[bold yellow]Warning:[/bold yellow] Some dependencies are missing: {', '.join(missing_deps)}\n" - ) - - console.print("[bold magenta]To upgrade using legacy validators:[/bold magenta]") - - # Build command with all validators in correct order - validator_args = " ".join( - f"--apply-legacy-validator {v.identifier}" for v in ordered_validators - ) - console.print(f" ado upgrade {resource_cli_name} {validator_args}") - console.print() - - # Show note about automatic dependency resolution - if len(ordered_validators) > len(validators): - console.print( - "[dim]Note: Additional validators were included to satisfy dependencies[/dim]\n" - ) - - console.print("[bold magenta]To list all legacy validators:[/bold magenta]") - console.print(f" ado upgrade {resource_cli_name} --list-legacy-validators") - - -# Made with Bob - - def extract_deprecated_field_paths( error: pydantic.ValidationError | ValueError, resource_type: "CoreResourceKinds | None" = None, @@ -131,8 +45,8 @@ def extract_deprecated_field_paths( - field error details: Maps full field path to list of error messages Raises: - ValueError: If error is a ValueError without a ValidationError cause and - resource_type is not provided + ValueError: Re-raises the original error if it is a ValueError without a + ValidationError cause and resource_type is not provided """ deprecated_field_paths: set[str] = set() field_errors: dict[str, list[str]] = {} @@ -168,9 +82,7 @@ def extract_deprecated_field_paths( # Fallback to simple string matching on error message if resource_type is None: - raise ValueError( - "resource_type is required for ValueError without ValidationError cause" - ) + raise error from orchestrator.core.legacy.registry import LegacyValidatorRegistry @@ -196,3 +108,89 @@ def extract_deprecated_field_paths( # Should not reach here due to type hints, but handle gracefully raise TypeError(f"Unsupported error type: {type(error)}") + + +def print_validator_suggestions_with_dependencies( + validators: list["LegacyValidatorMetadata"], + resource_type: "CoreResourceKinds", + console: Console, +) -> None: + """Print legacy validator suggestions with dependency information + + This enhanced version resolves dependencies and shows validators in the + correct execution order, along with dependency information. + + Args: + validators: List of applicable validators + resource_type: The resource type + console: Rich console to print to + """ + from orchestrator.core.legacy.registry import LegacyValidatorRegistry + + # Get validator identifiers + validator_ids = [v.identifier for v in validators] + missing_deps = [] + + # Resolve dependencies to get correct order + try: + validator_ids, missing_deps = LegacyValidatorRegistry.resolve_dependencies( + validator_ids + ) + except ValueError as e: + # Circular dependency detected + console_print(f"{ERROR}:{e}", stderr=True) + + # Get ordered validators (filter out None values) + ordered_validators: list[LegacyValidatorMetadata] = [] + for vid in validator_ids: + validator = LegacyValidatorRegistry.get_validator(vid) + if validator is not None: + ordered_validators.append(validator) + + console_print( + f"{INFO}The following validator(s) match the field(s) above:\n", stderr=True + ) + for i, validator in enumerate(ordered_validators, 1): + # Show execution order + console_print(f" {i}. [green]{validator.identifier}[/green]") + console_print(f" {validator.description}") + console_print(f" Handles: {', '.join(validator.deprecated_field_paths)}") + + # Show dependencies if any + if validator.dependencies: + dep_names = [] + for dep_id in validator.dependencies: + dep_validator = LegacyValidatorRegistry.get_validator(dep_id) + if dep_validator: + dep_names.append(dep_validator.identifier) + else: + dep_names.append(f"{dep_id} [red](missing)[/red]") + console_print(f" Dependencies: {', '.join(dep_names)}") + + console_print() + + # Warn about missing dependencies + if missing_deps: + console_print( + f"{WARN}Some dependencies are missing: {', '.join(missing_deps)}\n" + ) + + # Build command with all validators in correct order + validator_args = " ".join( + f"--apply-legacy-validator {v.identifier}" for v in ordered_validators + ) + console_print( + f"{HINT}To attempt the upgrade using the suggested legacy validator(s) run:\n" + f"\t{cyan(f'ado upgrade {resource_type.value} {validator_args}')}\n" + ) + + # Show note about automatic dependency resolution + if len(ordered_validators) > len(validators): + console_print( + "[dim]Note: Additional validators were included to satisfy dependencies[/dim]\n" + ) + + console_print( + f"{HINT}To list all legacy validators run:\n" + f"\t{cyan(f'ado upgrade {resource_type.value} --list-legacy-validators')}" + ) diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 39ac719ec..1e33309d3 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -408,6 +408,7 @@ def handle_ado_upgrade( kind=resource_type.value, ignore_validation_errors=False ) except ValueError as err: + status.stop() # Validation error occurred - check if legacy validators can help _handle_upgrade_validation_error(err, resource_type, parameters) raise typer.Exit(1) from err From 7be73968ed854d0d0a1e35f2efb5697d9d05b91c Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 12:52:57 +0000 Subject: [PATCH 33/55] refactor(cli): consolidate prints Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/legacy/common.py | 22 +++------ orchestrator/cli/utils/legacy/list.py | 62 ++++--------------------- orchestrator/core/legacy/metadata.py | 52 +++++++++++++++++++++ 3 files changed, 68 insertions(+), 68 deletions(-) diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index 3e806cffe..d10b4493d 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -151,22 +151,12 @@ def print_validator_suggestions_with_dependencies( f"{INFO}The following validator(s) match the field(s) above:\n", stderr=True ) for i, validator in enumerate(ordered_validators, 1): - # Show execution order - console_print(f" {i}. [green]{validator.identifier}[/green]") - console_print(f" {validator.description}") - console_print(f" Handles: {', '.join(validator.deprecated_field_paths)}") - - # Show dependencies if any - if validator.dependencies: - dep_names = [] - for dep_id in validator.dependencies: - dep_validator = LegacyValidatorRegistry.get_validator(dep_id) - if dep_validator: - dep_names.append(dep_validator.identifier) - else: - dep_names.append(f"{dep_id} [red](missing)[/red]") - console_print(f" Dependencies: {', '.join(dep_names)}") - + # Format and print validator info using the method + console_print( + validator.format_info( + index=i, show_dependencies=True, show_version_info=False + ) + ) console_print() # Warn about missing dependencies diff --git a/orchestrator/cli/utils/legacy/list.py b/orchestrator/cli/utils/legacy/list.py index bb7680770..cec7c8a15 100644 --- a/orchestrator/cli/utils/legacy/list.py +++ b/orchestrator/cli/utils/legacy/list.py @@ -3,9 +3,7 @@ """Utilities for listing legacy validators""" -from rich.console import Console -from rich.panel import Panel - +from orchestrator.cli.utils.output.prints import console_print from orchestrator.core.legacy.registry import LegacyValidatorRegistry from orchestrator.core.resources import CoreResourceKinds @@ -16,8 +14,6 @@ def list_legacy_validators(resource_type: CoreResourceKinds) -> None: Args: resource_type: The resource type to list validators for """ - console = Console() - # Import validators package to trigger registration via __init__.py import orchestrator.core.legacy.validators # noqa: F401 @@ -25,7 +21,7 @@ def list_legacy_validators(resource_type: CoreResourceKinds) -> None: validators = LegacyValidatorRegistry.get_validators_for_resource(resource_type) if not validators: - console.print( + console_print( f"\n[yellow]No legacy validators available for {resource_type.value}[/yellow]\n" ) return @@ -34,54 +30,16 @@ def list_legacy_validators(resource_type: CoreResourceKinds) -> None: # from cli_shorthands_to_cli_names in orchestrator/cli/utils/resources/mappings.py resource_cli_name = resource_type.value - console.print( - f"\n[bold cyan]Available legacy validators for {resource_cli_name}:[/bold cyan]\n" - ) - - for validator in validators: - # Create a panel for each validator - content_lines = [] - - # Description - content_lines.append("[bold]Description:[/bold]") - content_lines.append(f" {validator.description}") - content_lines.append("") - - # Deprecated fields - content_lines.append("[bold]Handles field paths:[/bold]") - content_lines.extend( - f" • {field}" for field in validator.deprecated_field_paths - ) - content_lines.append("") - - # Version info - content_lines.append("[bold]Version info:[/bold]") - content_lines.append( - f" Deprecated from: [cyan]{validator.deprecated_from_version}[/cyan]" - ) - content_lines.append( - f" Removed from: [cyan]{validator.removed_from_version}[/cyan]" - ) - content_lines.append("") - - # Usage - content_lines.append("[bold]Usage:[/bold]") - content_lines.append( - f" [green]ado upgrade {resource_cli_name} --apply-legacy-validator {validator.identifier}[/green]" - ) + console_print(f"Available legacy validators for {resource_cli_name}s:\n") - panel = Panel( - "\n".join(content_lines), - title=f"[bold magenta]{validator.identifier}[/bold magenta]", - border_style="cyan", - expand=False, + for i, validator in enumerate(validators, 1): + # Format and print validator info with version information using the method + console_print( + validator.format_info( + index=i, show_dependencies=True, show_version_info=False + ) ) - console.print(panel) - console.print() # Add spacing between panels - - console.print( - f"[bold]Found {len(validators)} legacy validator(s) for {resource_cli_name}[/bold]\n" - ) + console_print() # Add spacing between validators # Made with Bob diff --git a/orchestrator/core/legacy/metadata.py b/orchestrator/core/legacy/metadata.py index f9980e632..2ae7897cb 100644 --- a/orchestrator/core/legacy/metadata.py +++ b/orchestrator/core/legacy/metadata.py @@ -68,5 +68,57 @@ class LegacyValidatorMetadata(pydantic.BaseModel): model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + def format_info( + self, + index: int | None = None, + show_dependencies: bool = True, + show_version_info: bool = False, + ) -> str: + """Format validator information as a string + + Args: + index: Optional index number to display (e.g., "1." for execution order) + show_dependencies: Whether to show dependency information + show_version_info: Whether to show version information + + Returns: + Formatted string with validator information + """ + from orchestrator.core.legacy.registry import LegacyValidatorRegistry + + lines = [] + + # Validator identifier with optional index + if index is not None: + lines.append(f" {index}. [green]{self.identifier}[/green]") + else: + lines.append(f"[green]{self.identifier}[/green]") + + # Description + lines.append(f" {self.description}") + + # Field paths + lines.append(f" Handles: {', '.join(self.deprecated_field_paths)}") + + # Dependencies + if show_dependencies and self.dependencies: + dep_names = [] + for dep_id in self.dependencies: + dep_validator = LegacyValidatorRegistry.get_validator(dep_id) + if dep_validator: + dep_names.append(dep_validator.identifier) + else: + dep_names.append(f"{dep_id} [red](missing)[/red]") + lines.append(f" Depends on: {', '.join(dep_names)}") + + # Version information + if show_version_info: + lines.append( + f" Deprecated from: [cyan]{self.deprecated_from_version}[/cyan]" + ) + lines.append(f" Removed from: [cyan]{self.removed_from_version}[/cyan]") + + return "\n".join(lines) + # Made with Bob From fbede116e37503c3908df78776bd7512ef3d9684 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 13:02:27 +0000 Subject: [PATCH 34/55] refactor: remove useless parameter Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/legacy/common.py | 10 ++-------- orchestrator/cli/utils/resources/handlers.py | 4 +--- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/orchestrator/cli/utils/legacy/common.py b/orchestrator/cli/utils/legacy/common.py index d10b4493d..c141457ad 100644 --- a/orchestrator/cli/utils/legacy/common.py +++ b/orchestrator/cli/utils/legacy/common.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING import pydantic -from rich.console import Console from orchestrator.cli.utils.output.prints import ( ERROR, @@ -111,9 +110,7 @@ def extract_deprecated_field_paths( def print_validator_suggestions_with_dependencies( - validators: list["LegacyValidatorMetadata"], - resource_type: "CoreResourceKinds", - console: Console, + validators: list["LegacyValidatorMetadata"], resource_type: "CoreResourceKinds" ) -> None: """Print legacy validator suggestions with dependency information @@ -123,7 +120,6 @@ def print_validator_suggestions_with_dependencies( Args: validators: List of applicable validators resource_type: The resource type - console: Rich console to print to """ from orchestrator.core.legacy.registry import LegacyValidatorRegistry @@ -147,9 +143,7 @@ def print_validator_suggestions_with_dependencies( if validator is not None: ordered_validators.append(validator) - console_print( - f"{INFO}The following validator(s) match the field(s) above:\n", stderr=True - ) + console_print(f"{INFO}The following validator(s) are a match:\n", stderr=True) for i, validator in enumerate(ordered_validators, 1): # Format and print validator info using the method console_print( diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 1e33309d3..4793b37a1 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -488,9 +488,7 @@ def _handle_upgrade_validation_error( if validators: print_validator_suggestions_with_dependencies( - validators=validators, - resource_type=resource_type, - console=console, + validators=validators, resource_type=resource_type ) else: console.print( From a4fc4c0b201158f575c0a036e36f21bcf4a546a0 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 13:09:22 +0000 Subject: [PATCH 35/55] refactor(cli): reorder upgrade handler Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/resources/handlers.py | 277 ++++++++++--------- 1 file changed, 141 insertions(+), 136 deletions(-) diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index 4793b37a1..b07ead126 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -242,9 +242,6 @@ def handle_ado_upgrade( parameters: Command parameters including legacy validator options resource_type: The type of resource to upgrade """ - # Import validators package to trigger registration via __init__.py - import orchestrator.core.legacy.validators # noqa: F401 - # Handle --list-legacy-validators flag if parameters.list_legacy_validators: from orchestrator.cli.utils.legacy.list import list_legacy_validators @@ -252,172 +249,180 @@ def handle_ado_upgrade( list_legacy_validators(resource_type) return - # Get legacy validators if specified - legacy_validators = None - if parameters.apply_legacy_validator: - from orchestrator.core.legacy.registry import LegacyValidatorRegistry + sql_store = get_sql_store( + project_context=parameters.ado_configuration.project_context + ) - # Validate all validator IDs exist and match resource type - invalid_validators = [] - mismatched_validators = [] - for validator_id in parameters.apply_legacy_validator: - validator = LegacyValidatorRegistry.get_validator(validator_id) - if validator is None: - invalid_validators.append(validator_id) - elif validator.resource_type != resource_type: - mismatched_validators.append( - (validator_id, validator.resource_type, resource_type) + # Normal upgrade path without legacy validators + if not parameters.apply_legacy_validator: + + with Status(ADO_SPINNER_QUERYING_DB) as status: + try: + resources = sql_store.getResourcesOfKind( + kind=resource_type.value, ignore_validation_errors=False ) + except ValueError as err: + status.stop() + # Validation error occurred - check if legacy validators can help + _handle_upgrade_validation_error(err, resource_type, parameters) + raise typer.Exit(1) from err + + for idx, resource in enumerate(resources.values()): + status.update( + ADO_SPINNER_SAVING_TO_DB + f" ({idx + 1}/{len(resources)})" + ) + sql_store.updateResource(resource=resource) + + console_print(SUCCESS) + return - if invalid_validators: + # The user has requested legacy validators + legacy_validators = None + # Import validators package to trigger registration via __init__.py + import orchestrator.core.legacy.validators # noqa: F401 + from orchestrator.core.legacy.registry import LegacyValidatorRegistry + + # Validate all validator IDs exist and match resource type + invalid_validators = [] + mismatched_validators = [] + for validator_id in parameters.apply_legacy_validator: + validator = LegacyValidatorRegistry.get_validator(validator_id) + if validator is None: + invalid_validators.append(validator_id) + elif validator.resource_type != resource_type: + mismatched_validators.append( + (validator_id, validator.resource_type, resource_type) + ) + + if invalid_validators: + console_print( + f"{ERROR}Unknown legacy validator(s): {', '.join(invalid_validators)}", + stderr=True, + ) + raise typer.Exit(1) + + if mismatched_validators: + for validator_id, validator_type, expected_type in mismatched_validators: console_print( - f"{ERROR}Unknown legacy validator(s): {', '.join(invalid_validators)}", + f"{ERROR}Validator '{validator_id}' is for {validator_type.value} resources, " + f"but you are upgrading {expected_type.value} resources", stderr=True, ) - raise typer.Exit(1) + raise typer.Exit(1) - if mismatched_validators: - for validator_id, validator_type, expected_type in mismatched_validators: - console_print( - f"{ERROR}Validator '{validator_id}' is for {validator_type.value} resources, " - f"but you are upgrading {expected_type.value} resources", - stderr=True, - ) - raise typer.Exit(1) + # Resolve dependencies and order validators + try: + ordered_ids, missing_deps = LegacyValidatorRegistry.resolve_dependencies( + parameters.apply_legacy_validator + ) - # Resolve dependencies and order validators - try: - ordered_ids, missing_deps = LegacyValidatorRegistry.resolve_dependencies( - parameters.apply_legacy_validator + if missing_deps: + console_print( + f"{ERROR}Missing validator dependencies: {', '.join(missing_deps)}", + stderr=True, ) + raise typer.Exit(1) - if missing_deps: - console_print( - f"{ERROR}Missing validator dependencies: {', '.join(missing_deps)}", - stderr=True, - ) - raise typer.Exit(1) + # Get validators in correct order + legacy_validators = [] + for validator_id in ordered_ids: + validator = LegacyValidatorRegistry.get_validator(validator_id) + if validator is not None: + legacy_validators.append(validator) - # Get validators in correct order - legacy_validators = [] - for validator_id in ordered_ids: - validator = LegacyValidatorRegistry.get_validator(validator_id) - if validator is not None: - legacy_validators.append(validator) - - # Log the ordering - if len(ordered_ids) > len(parameters.apply_legacy_validator): - logger.info( - f"Auto-included dependencies: {[vid for vid in ordered_ids if vid not in parameters.apply_legacy_validator]}" - ) + # Log the ordering + if len(ordered_ids) > len(parameters.apply_legacy_validator): + logger.info( + f"Auto-included dependencies: {[vid for vid in ordered_ids if vid not in parameters.apply_legacy_validator]}" + ) - logger.debug( - f"Validators in execution order: {[v.identifier for v in legacy_validators]}" + if not legacy_validators: + console_print( + f"{ERROR}No validators were found using the provided identifiers" ) + raise typer.Exit(1) - except ValueError as e: - # Circular dependency detected - console_print(f"{ERROR}{e}", stderr=True) - raise typer.Exit(1) from e + logger.debug( + f"Validators in execution order: {[v.identifier for v in legacy_validators]}" + ) - sql_store = get_sql_store( - project_context=parameters.ado_configuration.project_context - ) + except ValueError as e: + # Circular dependency detected + console_print(f"{ERROR}{e}", stderr=True) + raise typer.Exit(1) from e # Import resource class mapping for validation from orchestrator.core import kindmap + # When legacy validators are specified, work with raw data with Status(ADO_SPINNER_QUERYING_DB) as status: - # When legacy validators are specified, work with raw data - if legacy_validators: - identifiers = sql_store.getResourceIdentifiersOfKind( - kind=resource_type.value - ) + identifiers = sql_store.getResourceIdentifiersOfKind(kind=resource_type.value) - # Phase 1: Collect and validate all migrations (transaction safety) - # Validate all resources before saving any to ensure atomicity - migrations = [] - resource_class = kindmap[resource_type.value] + # Phase 1: Collect and validate all migrations (transaction safety) + # Validate all resources before saving any to ensure atomicity + migrations = [] + resource_class = kindmap[resource_type.value] - for idx, identifier in enumerate(identifiers["IDENTIFIER"]): - status.update( - ADO_SPINNER_QUERYING_DB - + f" - Validating ({idx + 1}/{len(identifiers)})" - ) + for idx, identifier in enumerate(identifiers["IDENTIFIER"]): + status.update( + ADO_SPINNER_QUERYING_DB + + f" - Validating ({idx + 1}/{len(identifiers)})" + ) + + # Get raw data + resource_dict = sql_store.getResourceRaw(identifier) + if resource_dict is None: + continue - # Get raw data - resource_dict = sql_store.getResourceRaw(identifier) - if resource_dict is None: - continue - - # Apply legacy validators - try: - for validator in legacy_validators: - logger.debug( - f"Applying validator: {validator.identifier} to {identifier}" - ) - resource_dict = validator.validator_function(resource_dict) - logger.debug( - f"Validator {validator.identifier} completed for {identifier}" - ) - - # Validate the migrated resource (don't save yet) - resource = resource_class.model_validate(resource_dict) - migrations.append((identifier, resource)) - - except Exception as e: - logger.error(f"Migration failed for {identifier}: {e}") - console_print( - f"{ERROR}Migration validation failed for {identifier}: {e}", - stderr=True, + # Apply legacy validators + try: + for validator in legacy_validators: + logger.debug( + f"Applying validator: {validator.identifier} to {identifier}" ) - console_print( - f"{ERROR}No resources were modified (all-or-nothing transaction safety)", - stderr=True, + resource_dict = validator.validator_function(resource_dict) + logger.debug( + f"Validator {validator.identifier} completed for {identifier}" ) - raise typer.Exit(1) from e - # Phase 2: All validations passed, now save all resources - logger.info( - f"All {len(migrations)} resources validated successfully, applying changes..." - ) + # Validate the migrated resource (don't save yet) + resource = resource_class.model_validate(resource_dict) + migrations.append((identifier, resource)) - for idx, (identifier, migrated_resource) in enumerate(migrations): - status.update( - ADO_SPINNER_SAVING_TO_DB + f" ({idx + 1}/{len(migrations)})" + except Exception as e: + logger.error(f"Migration failed for {identifier}: {e}") + console_print( + f"{ERROR}Migration validation failed for {identifier}: {e}", + stderr=True, ) + console_print( + f"{ERROR}No resources were modified (all-or-nothing transaction safety)", + stderr=True, + ) + raise typer.Exit(1) from e + + # Phase 2: All validations passed, now save all resources + logger.info( + f"All {len(migrations)} resources validated successfully, applying changes..." + ) + + for idx, (identifier, migrated_resource) in enumerate(migrations): + status.update(ADO_SPINNER_SAVING_TO_DB + f" ({idx + 1}/{len(migrations)})") - try: - sql_store.updateResource(resource=migrated_resource) - except Exception as e: - logger.error(f"Failed to save {identifier}: {e}") - console_print( - f"{ERROR}Failed to save {identifier}. Database may be in inconsistent state.", - stderr=True, - ) - console_print( - f"{ERROR}Manual intervention may be required to restore consistency.", - stderr=True, - ) - raise typer.Exit(1) from e - else: - # Normal upgrade path without legacy validators try: - resources = sql_store.getResourcesOfKind( - kind=resource_type.value, ignore_validation_errors=False + sql_store.updateResource(resource=migrated_resource) + except Exception as e: + logger.error(f"Failed to save {identifier}: {e}") + console_print( + f"{ERROR}Failed to save {identifier}. Database may be in inconsistent state.", + stderr=True, ) - except ValueError as err: - status.stop() - # Validation error occurred - check if legacy validators can help - _handle_upgrade_validation_error(err, resource_type, parameters) - raise typer.Exit(1) from err - - for idx, resource in enumerate(resources.values()): - status.update( - ADO_SPINNER_SAVING_TO_DB + f" ({idx + 1}/{len(resources)})" + console_print( + f"{ERROR}Manual intervention may be required to restore consistency.", + stderr=True, ) - sql_store.updateResource(resource=resource) + raise typer.Exit(1) from e console_print(SUCCESS) From 1aac034dde98609cd39d744a9e23fa21758037d7 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 13:57:12 +0000 Subject: [PATCH 36/55] refactor: improve readability Signed-off-by: Alessandro Pomponio --- .../entitysource_to_samplestore.py | 2 - .../randomwalk_mode_to_sampler_config.py | 53 +++++------------ .../samplestore/entitysource_migrations.py | 24 ++++---- .../samplestore/v1_to_v2_csv_migration.py | 20 +++---- tests/core/test_validator_scope_fixes.py | 59 +++++++++++++++++++ 5 files changed, 98 insertions(+), 60 deletions(-) diff --git a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py index 583d43ee9..3c82b8268 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py @@ -52,9 +52,7 @@ def rename_entitysource_identifier(data: dict) -> dict: parent, field_name = get_nested_value(data, old_path) if parent is not None and field_name in parent: old_value = parent[field_name] - # Set the new value set_nested_value(data, new_path, old_value) - # Remove the old field remove_nested_field(data, old_path) return data diff --git a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py index ee00d3307..c2cfa5dc4 100644 --- a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py +++ b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py @@ -44,43 +44,22 @@ def migrate_randomwalk_to_sampler_config(data: dict) -> dict: if not isinstance(data, dict): return data - # Check if mode field exists (indicator of old format) - if not has_nested_field(data, "config.parameters.mode"): - return data - - # Extract the old fields - has_nested_field already confirmed they exist - mode = None - grouping = None - sampler_type = None - - if has_nested_field(data, "config.parameters.mode"): - parent, field = get_nested_value(data, "config.parameters.mode") - if parent is not None: - mode = parent[field] - remove_nested_field(data, "config.parameters.mode") - - if has_nested_field(data, "config.parameters.grouping"): - parent, field = get_nested_value(data, "config.parameters.grouping") - if parent is not None: - grouping = parent[field] - remove_nested_field(data, "config.parameters.grouping") - - if has_nested_field(data, "config.parameters.samplerType"): - parent, field = get_nested_value(data, "config.parameters.samplerType") - if parent is not None: - sampler_type = parent[field] - remove_nested_field(data, "config.parameters.samplerType") - - # Create samplerConfig if any of the fields were present - if mode is not None or grouping is not None or sampler_type is not None: - sampler_config = {} - if mode is not None: - sampler_config["mode"] = mode - if grouping is not None: - sampler_config["grouping"] = grouping - if sampler_type is not None: - sampler_config["samplerType"] = sampler_type - + # Fields to migrate from top-level parameters to samplerConfig + fields_to_migrate = ["mode", "grouping", "samplerType"] + + sampler_config = {} + + # Extract and migrate each field + for field_name in fields_to_migrate: + field_path = f"config.parameters.{field_name}" + if has_nested_field(data, field_path): + parent, field = get_nested_value(data, field_path) + if parent is not None: + sampler_config[field_name] = parent[field] + remove_nested_field(data, field_path) + + # Only set samplerConfig if we found any fields to migrate + if sampler_config: set_nested_value(data, "config.parameters.samplerConfig", sampler_config) return data diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index 8e6ebdb2c..056ba222a 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -111,17 +111,20 @@ def migrate_module_name(data: dict) -> dict: """Convert moduleName paths from entitysource to samplestore This validator checks for moduleName field within the config - and converts paths from entitysource to samplestore. + and converts paths from entitysource to samplestore using exact matching. - Old format: + Only exact matches are migrated: config: - moduleName: "orchestrator.core.entitysource.*" - moduleName: "orchestrator.plugins.entitysources.*" + moduleName: "orchestrator.core.entitysource" + -> "orchestrator.core.samplestore" - New format: + moduleName: "orchestrator.plugins.entitysources" + -> "orchestrator.plugins.samplestores" + + Submodules or partial matches are NOT migrated: config: - moduleName: "orchestrator.core.samplestore.*" - moduleName: "orchestrator.plugins.samplestores.*" + moduleName: "orchestrator.core.entitysource.csv" + -> unchanged (not an exact match) Args: data: The resource data dictionary @@ -143,10 +146,9 @@ def migrate_module_name(data: dict) -> dict: parent, field = get_nested_value(data, "config.moduleName") if parent is not None and isinstance(parent[field], str): module_name = parent[field] - for old_path, new_path in path_mappings.items(): - if old_path in module_name: - module_name = module_name.replace(old_path, new_path) - set_nested_value(data, "config.moduleName", module_name) + if module_name in path_mappings: + module_name = path_mappings[module_name] + set_nested_value(data, "config.moduleName", module_name) return data diff --git a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py index 2bec84a2b..f4d94750c 100644 --- a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py +++ b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py @@ -4,7 +4,7 @@ """Legacy validator for migrating CSV sample stores from v1 to v2 format""" from orchestrator.core.legacy.registry import legacy_validator -from orchestrator.core.legacy.utils import has_nested_field +from orchestrator.core.legacy.utils import get_nested_value, has_nested_field from orchestrator.core.resources import CoreResourceKinds @@ -48,22 +48,22 @@ def migrate_csv_v1_to_v2(data: dict) -> dict: if not isinstance(data, dict): return data - # Check if config exists - if "config" not in data or not isinstance(data["config"], dict): - return data - - config = data["config"] - # Check if this is old format (has constitutivePropertyColumns in config) if not has_nested_field(data, "config.constitutivePropertyColumns"): return data - # Extract and remove the constitutivePropertyColumns from config + # Get config parent (data dict) and field name to access config + parent, field = get_nested_value(data, "config") + if parent is None or field is None or field not in parent: + return data + + config = parent[field] constitutive_columns = config.pop("constitutivePropertyColumns") # Migrate experiments if present in config - if "experiments" in config and isinstance(config["experiments"], list): - for exp in config["experiments"]: + experiments = config.get("experiments") + if isinstance(experiments, list): + for exp in experiments: if isinstance(exp, dict): # Rename propertyMap to observedPropertyMap if "propertyMap" in exp: diff --git a/tests/core/test_validator_scope_fixes.py b/tests/core/test_validator_scope_fixes.py index f46097e62..225aa706b 100644 --- a/tests/core/test_validator_scope_fixes.py +++ b/tests/core/test_validator_scope_fixes.py @@ -304,5 +304,64 @@ def test_validators_handle_empty_config_gracefully(self) -> None: # Verify: data unchanged assert result == resource_data + def test_samplestore_module_name_exact_match(self) -> None: + """Verify module name migration uses exact matching only""" + + # Get validator + validator = LegacyValidatorRegistry.get_validator( + "samplestore_module_name_entitysource_to_samplestore" + ) + assert validator is not None + + # Test data with module names that should NOT be migrated + # (not exact matches) + no_migration_cases = [ + "my_orchestrator.core.entitysource_wrapper", # Contains substring but not exact + "orchestrator.core.entitysource.csv", # Submodule, not exact match + "orchestrator.plugins.entitysources.custom", # Submodule, not exact match + ] + + for module_name in no_migration_cases: + data = { + "kind": "samplestore", + "type": "csv", + "identifier": "test-store", + "config": { + "moduleName": module_name, + }, + } + result = validator.validator_function(data.copy()) + assert result["config"]["moduleName"] == module_name, ( + f"Expected {module_name} to NOT be migrated, " + f"but got {result['config']['moduleName']}" + ) + + # Test exact matches that SHOULD migrate + exact_match_cases = [ + ( + "orchestrator.core.entitysource", + "orchestrator.core.samplestore", + ), + ( + "orchestrator.plugins.entitysources", + "orchestrator.plugins.samplestores", + ), + ] + + for old_name, expected_new_name in exact_match_cases: + data = { + "kind": "samplestore", + "type": "csv", + "identifier": "test-store", + "config": { + "moduleName": old_name, + }, + } + result = validator.validator_function(data.copy()) + assert result["config"]["moduleName"] == expected_new_name, ( + f"Expected {old_name} to migrate to {expected_new_name}, " + f"but got {result['config']['moduleName']}" + ) + # Made with Bob From e65336ca707d4e563d87c844b53266d82a48485e Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 14:03:26 +0000 Subject: [PATCH 37/55] refactor: update function name Signed-off-by: Alessandro Pomponio --- orchestrator/core/legacy/utils.py | 34 ++++++++++-- .../entitysource_to_samplestore.py | 5 +- .../randomwalk_mode_to_sampler_config.py | 6 +- .../samplestore/entitysource_migrations.py | 19 +++---- .../samplestore/v1_to_v2_csv_migration.py | 8 +-- tests/core/test_legacy_utils.py | 55 +++++++++++++++++-- 6 files changed, 94 insertions(+), 33 deletions(-) diff --git a/orchestrator/core/legacy/utils.py b/orchestrator/core/legacy/utils.py index db1695c96..9d22a6485 100644 --- a/orchestrator/core/legacy/utils.py +++ b/orchestrator/core/legacy/utils.py @@ -4,9 +4,12 @@ """Utility functions for legacy validators""" -def get_nested_value(data: dict, path: str) -> tuple[dict | None, str | None]: +def get_parent_dict_and_key(data: dict, path: str) -> tuple[dict | None, str | None]: """Navigate to a nested field path and return parent dict and field name + This is a low-level helper used by set_nested_value, remove_nested_field, + and has_nested_field. For reading values, use get_nested_value instead. + Args: data: The data dictionary path: Dot-separated path (e.g., "config.specification.module.moduleType") @@ -15,7 +18,7 @@ def get_nested_value(data: dict, path: str) -> tuple[dict | None, str | None]: Tuple of (parent_dict, field_name) or (None, None) if path doesn't exist Example: - parent, field = get_nested_value(data, "config.properties") + parent, field = get_parent_dict_and_key(data, "config.properties") if parent and field: parent.pop(field, None) """ @@ -35,6 +38,27 @@ def get_nested_value(data: dict, path: str) -> tuple[dict | None, str | None]: return None, None +def get_nested_value(data: dict, path: str) -> object | None: + """Get the value at a nested field path + + Args: + data: The data dictionary + path: Dot-separated path (e.g., "config.specification.module.moduleType") + + Returns: + The value at the specified path, or None if path doesn't exist + + Example: + value = get_nested_value(data, "config.moduleType") + if value == "sample_store": + # Do something + """ + parent, field = get_parent_dict_and_key(data, path) + if parent is not None and field is not None and field in parent: + return parent[field] + return None + + def set_nested_value(data: dict, path: str, value: object) -> bool: """Set a value at a nested field path @@ -51,7 +75,7 @@ def set_nested_value(data: dict, path: str, value: object) -> bool: set_nested_value(data, "config.specification.module.type", "sample_store") # data is now {"config": {"specification": {"module": {"type": "sample_store"}}}} """ - parent, field = get_nested_value(data, path) + parent, field = get_parent_dict_and_key(data, path) if parent is not None and field is not None: parent[field] = value return True @@ -73,7 +97,7 @@ def remove_nested_field(data: dict, path: str) -> bool: remove_nested_field(data, "config.properties") # data is now {"config": {"other": "value"}} """ - parent, field = get_nested_value(data, path) + parent, field = get_parent_dict_and_key(data, path) if parent is not None and field is not None and field in parent: parent.pop(field) return True @@ -95,7 +119,7 @@ def has_nested_field(data: dict, path: str) -> bool: has_nested_field(data, "config.specification.module.moduleType") # Returns True has_nested_field(data, "config.nonexistent") # Returns False """ - parent, field = get_nested_value(data, path) + parent, field = get_parent_dict_and_key(data, path) return parent is not None and field is not None and field in parent diff --git a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py index 3c82b8268..9769b0813 100644 --- a/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/discoveryspace/entitysource_to_samplestore.py @@ -49,9 +49,8 @@ def rename_entitysource_identifier(data: dict) -> dict: new_path = "config.sampleStoreIdentifier" # Get the old value if it exists - parent, field_name = get_nested_value(data, old_path) - if parent is not None and field_name in parent: - old_value = parent[field_name] + old_value = get_nested_value(data, old_path) + if old_value is not None: set_nested_value(data, new_path, old_value) remove_nested_field(data, old_path) diff --git a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py index c2cfa5dc4..3bc51f4e1 100644 --- a/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py +++ b/orchestrator/core/legacy/validators/operation/randomwalk_mode_to_sampler_config.py @@ -53,9 +53,9 @@ def migrate_randomwalk_to_sampler_config(data: dict) -> dict: for field_name in fields_to_migrate: field_path = f"config.parameters.{field_name}" if has_nested_field(data, field_path): - parent, field = get_nested_value(data, field_path) - if parent is not None: - sampler_config[field_name] = parent[field] + field_value = get_nested_value(data, field_path) + if field_value is not None: + sampler_config[field_name] = field_value remove_nested_field(data, field_path) # Only set samplerConfig if we found any fields to migrate diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index 056ba222a..684da3e21 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -46,8 +46,8 @@ def migrate_module_type(data: dict) -> dict: # Check and update config.moduleType if has_nested_field(data, "config.moduleType"): - parent, field = get_nested_value(data, "config.moduleType") - if parent is not None and parent[field] == "entity_source": + module_type = get_nested_value(data, "config.moduleType") + if module_type == "entity_source": set_nested_value(data, "config.moduleType", "sample_store") return data @@ -92,9 +92,9 @@ def migrate_module_class(data: dict) -> dict: # Check and update config.moduleClass if has_nested_field(data, "config.moduleClass"): - parent, field = get_nested_value(data, "config.moduleClass") - if parent is not None and parent[field] in value_mappings: - set_nested_value(data, "config.moduleClass", value_mappings[parent[field]]) + module_class = get_nested_value(data, "config.moduleClass") + if isinstance(module_class, str) and module_class in value_mappings: + set_nested_value(data, "config.moduleClass", value_mappings[module_class]) return data @@ -143,12 +143,9 @@ def migrate_module_name(data: dict) -> dict: # Check and update config.moduleName if has_nested_field(data, "config.moduleName"): - parent, field = get_nested_value(data, "config.moduleName") - if parent is not None and isinstance(parent[field], str): - module_name = parent[field] - if module_name in path_mappings: - module_name = path_mappings[module_name] - set_nested_value(data, "config.moduleName", module_name) + module_name = get_nested_value(data, "config.moduleName") + if isinstance(module_name, str) and module_name in path_mappings: + set_nested_value(data, "config.moduleName", path_mappings[module_name]) return data diff --git a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py index f4d94750c..d499f99b5 100644 --- a/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py +++ b/orchestrator/core/legacy/validators/samplestore/v1_to_v2_csv_migration.py @@ -52,14 +52,12 @@ def migrate_csv_v1_to_v2(data: dict) -> dict: if not has_nested_field(data, "config.constitutivePropertyColumns"): return data - # Get config parent (data dict) and field name to access config - parent, field = get_nested_value(data, "config") - if parent is None or field is None or field not in parent: + # Get config value + config = get_nested_value(data, "config") + if config is None or not isinstance(config, dict): return data - config = parent[field] constitutive_columns = config.pop("constitutivePropertyColumns") - # Migrate experiments if present in config experiments = config.get("experiments") if isinstance(experiments, list): diff --git a/tests/core/test_legacy_utils.py b/tests/core/test_legacy_utils.py index 69bd21b6f..4ac2a592d 100644 --- a/tests/core/test_legacy_utils.py +++ b/tests/core/test_legacy_utils.py @@ -5,44 +5,87 @@ from orchestrator.core.legacy.utils import ( get_nested_value, + get_parent_dict_and_key, has_nested_field, remove_nested_field, set_nested_value, ) -class TestGetNestedValue: - """Tests for get_nested_value function""" +class TestGetParentDictAndKey: + """Tests for get_parent_dict_and_key function""" def test_simple_path(self) -> None: """Test getting a simple top-level field""" data = {"config": {"properties": ["a", "b"]}} - parent, field = get_nested_value(data, "config") + parent, field = get_parent_dict_and_key(data, "config") assert parent == data assert field == "config" def test_nested_path(self) -> None: """Test getting a nested field""" data = {"config": {"specification": {"module": {"moduleType": "test"}}}} - parent, field = get_nested_value(data, "config.specification.module.moduleType") + parent, field = get_parent_dict_and_key( + data, "config.specification.module.moduleType" + ) assert parent == {"moduleType": "test"} assert field == "moduleType" def test_nonexistent_path(self) -> None: """Test getting a path that doesn't exist""" data = {"config": {}} - parent, field = get_nested_value(data, "config.nonexistent.field") + parent, field = get_parent_dict_and_key(data, "config.nonexistent.field") assert parent is None assert field is None def test_path_through_non_dict(self) -> None: """Test path that goes through a non-dict value""" data = {"config": "string_value"} - parent, field = get_nested_value(data, "config.field") + parent, field = get_parent_dict_and_key(data, "config.field") assert parent is None assert field is None +class TestGetNestedValue: + """Tests for get_nested_value function""" + + def test_simple_path(self) -> None: + """Test getting a simple top-level value""" + data = {"config": {"properties": ["a", "b"]}} + value = get_nested_value(data, "config.properties") + assert value == ["a", "b"] + + def test_nested_path(self) -> None: + """Test getting a nested value""" + data = {"config": {"specification": {"module": {"moduleType": "test"}}}} + value = get_nested_value(data, "config.specification.module.moduleType") + assert value == "test" + + def test_nonexistent_path(self) -> None: + """Test getting a path that doesn't exist""" + data = {"config": {}} + value = get_nested_value(data, "config.nonexistent.field") + assert value is None + + def test_path_through_non_dict(self) -> None: + """Test path that goes through a non-dict value""" + data = {"config": "string_value"} + value = get_nested_value(data, "config.field") + assert value is None + + def test_get_dict_value(self) -> None: + """Test getting a dict value""" + data = {"config": {"nested": {"key": "value"}}} + value = get_nested_value(data, "config.nested") + assert value == {"key": "value"} + + def test_get_none_value(self) -> None: + """Test getting a field that exists but has None value""" + data = {"config": {"test": None}} + value = get_nested_value(data, "config.test") + assert value is None + + class TestSetNestedValue: """Tests for set_nested_value function""" From 5778cb4b1bca443c607f19d3bb37776320623d8f Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 14:43:47 +0000 Subject: [PATCH 38/55] test: avoid mocks Signed-off-by: Alessandro Pomponio --- tests/core/test_legacy_validators.py | 367 +++++++++++++++++---------- 1 file changed, 234 insertions(+), 133 deletions(-) diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index 9157bf066..86aa35d67 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -3,13 +3,14 @@ """Integration tests for legacy validators with pydantic models and upgrade process""" -from unittest.mock import MagicMock, patch +from collections.abc import Callable +from pathlib import Path import pydantic -import pytest from orchestrator.core.legacy.registry import LegacyValidatorRegistry, legacy_validator from orchestrator.core.resources import CoreResourceKinds +from orchestrator.metastore.project import ProjectContext class TestLegacyValidatorWithPydantic: @@ -195,16 +196,46 @@ def step2(data: dict) -> dict: class TestUpgradeHandlerIntegration: - """Test the upgrade handler with legacy validators""" + """Integration tests for ado upgrade with legacy validators via CLI""" def setup_method(self) -> None: - """Clear the registry before each test""" - LegacyValidatorRegistry._validators = {} + """Setup - ensure validators are registered""" + import orchestrator.core.legacy.validators # noqa: F401 + + if not hasattr(self.__class__, "_initial_validators"): + self.__class__._initial_validators = ( + LegacyValidatorRegistry._validators.copy() + ) + elif not LegacyValidatorRegistry._validators: + LegacyValidatorRegistry._validators = ( + self.__class__._initial_validators.copy() + ) + + def test_upgrade_applies_legacy_validator_via_cli( + self, + tmp_path: Path, + valid_ado_project_context: ProjectContext, + create_active_ado_context: Callable, + ) -> None: + """Test that ado upgrade applies legacy validators correctly via CLI""" + + from typer.testing import CliRunner + + runner = CliRunner() + + from orchestrator.cli.core.cli import app as ado + from orchestrator.cli.utils.generic.wrappers import get_sql_store + from orchestrator.core.samplestore.config import ( + SampleStoreConfiguration, + SampleStoreModuleConf, + SampleStoreSpecification, + ) + from orchestrator.core.samplestore.resource import SampleStoreResource - def test_upgrade_handler_applies_legacy_validator(self) -> None: - """Test that handle_ado_upgrade applies legacy validators correctly""" + # Step 1: Setup active context + create_active_ado_context(runner, tmp_path, valid_ado_project_context) - # Register a test validator + # Step 2: Register a test validator @legacy_validator( identifier="test_upgrade_validator", resource_type=CoreResourceKinds.SAMPLESTORE, @@ -214,156 +245,226 @@ def test_upgrade_handler_applies_legacy_validator(self) -> None: description="Test upgrade validator", ) def test_validator(data: dict) -> dict: - if "old_field" in data: - data["new_field"] = data.pop("old_field") + """Migrate old_field to new_field""" + if "config" in data and "old_field" in data["config"]: + data["config"]["new_field"] = data["config"].pop("old_field") return data - # Create a mock resource class with model_validate - mock_resource_class = MagicMock() - mock_validated_resource = MagicMock() - mock_resource_class.model_validate.return_value = mock_validated_resource - - mock_sql_store = MagicMock() - # Mock getResourceIdentifiersOfKind to return identifiers - mock_sql_store.getResourceIdentifiersOfKind.return_value = { - "IDENTIFIER": ["res1"] - } - # Mock getResourceRaw to return raw dict data - mock_sql_store.getResourceRaw.return_value = {"old_field": "test_value"} - - # Mock parameters - mock_params = MagicMock() - mock_params.apply_legacy_validator = ["test_upgrade_validator"] - mock_params.list_legacy_validators = False - mock_params.ado_configuration.project_context = "test_context" - - # Patch dependencies including kindmap - with ( - patch( - "orchestrator.cli.utils.resources.handlers.get_sql_store", - return_value=mock_sql_store, - ), - patch("orchestrator.cli.utils.resources.handlers.Status"), - patch("orchestrator.cli.utils.resources.handlers.console_print"), - patch( - "orchestrator.core.kindmap", - {CoreResourceKinds.SAMPLESTORE.value: mock_resource_class}, + # Step 3: Create a sample store resource + test_resource = SampleStoreResource( + identifier="test_legacy_store", + config=SampleStoreConfiguration( + specification=SampleStoreSpecification( + module=SampleStoreModuleConf( + moduleClass="SQLSampleStore", + moduleName="orchestrator.core.samplestore.sql", + ), + storageLocation=valid_ado_project_context.metadataStore, + ) ), - ): - from orchestrator.cli.utils.resources.handlers import ( - handle_ado_upgrade, - ) + ) - # Call the upgrade handler - handle_ado_upgrade( - parameters=mock_params, - resource_type=CoreResourceKinds.SAMPLESTORE, - ) + # Step 4: Save resource to database + sql_store = get_sql_store(project_context=valid_ado_project_context) + sql_store.updateResource(resource=test_resource) + + # Step 5: Execute upgrade via CLI + result = runner.invoke( + ado, + [ + "--override-ado-app-dir", + str(tmp_path), + "upgrade", + "samplestore", + "--apply-legacy-validator", + "test_upgrade_validator", + ], + ) + + # Step 6: Verify success + assert result.exit_code == 0 + assert "Success" in result.output or "✓" in result.output + + # Step 7: Verify the upgrade process completed successfully + # The CLI output "Success!" confirms the validator was applied + # and the resource was upgraded in the database + + def test_upgrade_rejects_mismatched_validator_type( + self, + tmp_path: Path, + valid_ado_project_context: ProjectContext, + create_active_ado_context: Callable, + ) -> None: + """Test that upgrade rejects validators for wrong resource type""" + + from typer.testing import CliRunner - # Verify the resource was processed - mock_sql_store.getResourceRaw.assert_called_once_with("res1") - mock_resource_class.model_validate.assert_called_once() - mock_sql_store.updateResource.assert_called_once() + runner = CliRunner() - def test_upgrade_handler_validates_validator_resource_type(self) -> None: - """Test that upgrade handler validates validator resource type matches""" + from orchestrator.cli.core.cli import app as ado - # Register a validator for OPERATION + # Step 1: Setup active context + create_active_ado_context(runner, tmp_path, valid_ado_project_context) + + # Step 2: Register a validator for OPERATION @legacy_validator( - identifier="operation_validator", + identifier="operation_only_validator", resource_type=CoreResourceKinds.OPERATION, deprecated_field_paths=["config.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", - description="Operation validator", + description="Operation-only validator", ) - def op_validator(data: dict) -> dict: + def operation_validator(data: dict) -> dict: return data - # Mock parameters trying to use operation validator on samplestore - mock_params = MagicMock() - mock_params.apply_legacy_validator = ["operation_validator"] - mock_params.list_legacy_validators = False - mock_params.ado_configuration.project_context = "test_context" + # Step 3: Try to use operation validator on samplestore + result = runner.invoke( + ado, + [ + "--override-ado-app-dir", + str(tmp_path), + "upgrade", + "samplestore", + "--apply-legacy-validator", + "operation_only_validator", + ], + ) - mock_sql_store = MagicMock() + # Step 4: Verify failure with appropriate error message + assert result.exit_code == 1 + assert "ERROR" in result.output + assert "operation_only_validator" in result.output + assert "operation" in result.output.lower() + assert "samplestore" in result.output.lower() + + def test_upgrade_rejects_unknown_validator( + self, + tmp_path: Path, + valid_ado_project_context: ProjectContext, + create_active_ado_context: Callable, + ) -> None: + """Test that upgrade rejects unknown validator identifiers""" + + from typer.testing import CliRunner + + runner = CliRunner() + + from orchestrator.cli.core.cli import app as ado + + # Step 1: Setup active context + create_active_ado_context(runner, tmp_path, valid_ado_project_context) + + # Step 2: Try to use non-existent validator + result = runner.invoke( + ado, + [ + "--override-ado-app-dir", + str(tmp_path), + "upgrade", + "samplestore", + "--apply-legacy-validator", + "nonexistent_validator_xyz", + ], + ) - # Patch dependencies - with ( - patch( - "orchestrator.cli.utils.resources.handlers.get_sql_store", - return_value=mock_sql_store, - ), - patch( - "orchestrator.cli.utils.resources.handlers.console_print" - ) as mock_print, - ): - import typer + # Step 3: Verify failure with appropriate error message + assert result.exit_code == 1 + assert "ERROR" in result.output + assert "nonexistent_validator_xyz" in result.output + assert ( + "unknown" in result.output.lower() or "not found" in result.output.lower() + ) - from orchestrator.cli.utils.resources.handlers import ( - handle_ado_upgrade, - ) + def test_upgrade_auto_resolves_validator_dependencies( + self, + tmp_path: Path, + valid_ado_project_context: ProjectContext, + create_active_ado_context: Callable, + ) -> None: + """Test that upgrade automatically includes validator dependencies""" - # Should raise typer.Exit - with pytest.raises(typer.Exit) as exc_info: - handle_ado_upgrade( - parameters=mock_params, - resource_type=CoreResourceKinds.SAMPLESTORE, - ) + from typer.testing import CliRunner - assert exc_info.value.exit_code == 1 - - # Verify error message was printed with correct resource type mismatch - mock_print.assert_called() - call_args = str(mock_print.call_args) - assert "operation_validator" in call_args - assert "operation" in call_args.lower() - assert "samplestore" in call_args.lower() - # Check for the specific error message format - assert "is for" in call_args.lower() or "upgrading" in call_args.lower() - - def test_upgrade_handler_validates_validator_exists(self) -> None: - """Test that upgrade handler validates validator exists""" - - # Mock parameters with non-existent validator - mock_params = MagicMock() - mock_params.apply_legacy_validator = ["nonexistent_validator"] - mock_params.list_legacy_validators = False - mock_params.ado_configuration.project_context = "test_context" - - mock_sql_store = MagicMock() - - # Patch dependencies - with ( - patch( - "orchestrator.cli.utils.resources.handlers.get_sql_store", - return_value=mock_sql_store, - ), - patch( - "orchestrator.cli.utils.resources.handlers.console_print" - ) as mock_print, - ): - import typer + runner = CliRunner() - from orchestrator.cli.utils.resources.handlers import ( - handle_ado_upgrade, - ) + from orchestrator.cli.core.cli import app as ado + from orchestrator.cli.utils.generic.wrappers import get_sql_store + from orchestrator.core.samplestore.config import ( + SampleStoreConfiguration, + SampleStoreModuleConf, + SampleStoreSpecification, + ) + from orchestrator.core.samplestore.resource import SampleStoreResource - # Should raise typer.Exit - with pytest.raises(typer.Exit) as exc_info: - handle_ado_upgrade( - parameters=mock_params, - resource_type=CoreResourceKinds.SAMPLESTORE, + # Step 1: Setup active context + create_active_ado_context(runner, tmp_path, valid_ado_project_context) + + # Step 2: Register validators with dependencies + @legacy_validator( + identifier="base_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_field_paths=["config.field1"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Base validator", + ) + def base_validator(data: dict) -> dict: + if "config" in data and "field1" in data["config"]: + data["config"]["field1_migrated"] = True + return data + + @legacy_validator( + identifier="dependent_validator", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_field_paths=["config.field2"], + deprecated_from_version="1.0.0", + removed_from_version="2.0.0", + description="Dependent validator", + dependencies=["base_validator"], # Depends on base_validator + ) + def dependent_validator(data: dict) -> dict: + if "config" in data and "field2" in data["config"]: + data["config"]["field2_migrated"] = True + return data + + # Step 3: Create and save a sample store resource + test_resource = SampleStoreResource( + identifier="test_dependency_store", + config=SampleStoreConfiguration( + specification=SampleStoreSpecification( + module=SampleStoreModuleConf( + moduleClass="SQLSampleStore", + moduleName="orchestrator.core.samplestore.sql", + ), + storageLocation=valid_ado_project_context.metadataStore, ) + ), + ) + + sql_store = get_sql_store(project_context=valid_ado_project_context) + sql_store.updateResource(resource=test_resource) + + # Step 4: Execute upgrade with only dependent_validator + # Should auto-include base_validator + result = runner.invoke( + ado, + [ + "--override-ado-app-dir", + str(tmp_path), + "upgrade", + "samplestore", + "--apply-legacy-validator", + "dependent_validator", + ], + ) - assert exc_info.value.exit_code == 1 + # Step 5: Verify success + assert result.exit_code == 0 + assert "Success" in result.output or "✓" in result.output - # Verify error message was printed - mock_print.assert_called() - call_args = str(mock_print.call_args) - assert "nonexistent_validator" in call_args - # Check for "unknown" instead of "not found" to match actual error message - assert "unknown" in call_args.lower() + # The test verifies the CLI command completes successfully + # with automatic dependency resolution class TestValidatorDataIntegrity: From dd28ea023ed494d3d3c8836806aa4eeb97a547cc Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 14:50:16 +0000 Subject: [PATCH 39/55] refactor(tests): use fixture for common metadata pattern Signed-off-by: Alessandro Pomponio --- tests/core/test_legacy_registry.py | 186 +++++++++++++---------------- 1 file changed, 85 insertions(+), 101 deletions(-) diff --git a/tests/core/test_legacy_registry.py b/tests/core/test_legacy_registry.py index fdc6ad7f6..dc1d569aa 100644 --- a/tests/core/test_legacy_registry.py +++ b/tests/core/test_legacy_registry.py @@ -3,6 +3,10 @@ """Unit tests for the legacy validator registry""" +from collections.abc import Callable + +import pytest + from orchestrator.core.legacy.metadata import LegacyValidatorMetadata from orchestrator.core.legacy.registry import ( LegacyValidatorRegistry, @@ -11,23 +15,65 @@ from orchestrator.core.resources import CoreResourceKinds +@pytest.fixture +def dummy_validator() -> Callable[[dict], dict]: + """Fixture providing a simple dummy validator function""" + + def validator(data: dict) -> dict: + return data + + return validator + + +@pytest.fixture +def create_validator_metadata( + dummy_validator: Callable[[dict], dict], +) -> Callable[..., LegacyValidatorMetadata]: + """Fixture factory for creating LegacyValidatorMetadata instances""" + + def _create_metadata( + identifier: str = "test_validator", + resource_type: CoreResourceKinds = CoreResourceKinds.SAMPLESTORE, + deprecated_field_paths: list[str] | None = None, + deprecated_from_version: str = "1.0.0", + removed_from_version: str = "2.0.0", + description: str = "Test validator", + validator_function: Callable[[dict], dict] | None = None, + dependencies: list[str] | None = None, + ) -> LegacyValidatorMetadata: + if deprecated_field_paths is None: + deprecated_field_paths = ["config.field1"] + if validator_function is None: + validator_function = dummy_validator + if dependencies is None: + dependencies = [] + + return LegacyValidatorMetadata( + identifier=identifier, + resource_type=resource_type, + deprecated_field_paths=deprecated_field_paths, + deprecated_from_version=deprecated_from_version, + removed_from_version=removed_from_version, + description=description, + validator_function=validator_function, + dependencies=dependencies, + ) + + return _create_metadata + + class TestLegacyValidatorMetadata: """Test the LegacyValidatorMetadata model""" - def test_create_metadata(self) -> None: + def test_create_metadata( + self, + create_validator_metadata: Callable[..., LegacyValidatorMetadata], + dummy_validator: Callable[[dict], dict], + ) -> None: """Test creating validator metadata""" - - def dummy_validator(data: dict) -> dict: - return data - - metadata = LegacyValidatorMetadata( + metadata = create_validator_metadata( identifier="test_validator", - resource_type=CoreResourceKinds.SAMPLESTORE, deprecated_field_paths=["config.field1", "config.field2"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", - description="Test validator", - validator_function=dummy_validator, ) assert metadata.identifier == "test_validator" @@ -41,21 +87,11 @@ def dummy_validator(data: dict) -> dict: assert metadata.description == "Test validator" assert metadata.validator_function == dummy_validator - def test_metadata_serialization(self) -> None: + def test_metadata_serialization( + self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + ) -> None: """Test that validator function is excluded from serialization""" - - def dummy_validator(data: dict) -> dict: - return data - - metadata = LegacyValidatorMetadata( - identifier="test_validator", - resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.field1"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", - description="Test validator", - validator_function=dummy_validator, - ) + metadata = create_validator_metadata() # Serialize to dict data = metadata.model_dump() @@ -73,42 +109,22 @@ def setup_method(self) -> None: """Clear the registry before each test""" LegacyValidatorRegistry._validators = {} - def test_register_validator(self) -> None: + def test_register_validator( + self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + ) -> None: """Test registering a validator""" - - def dummy_validator(data: dict) -> dict: - return data - - metadata = LegacyValidatorMetadata( - identifier="test_validator", - resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.field1"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", - description="Test validator", - validator_function=dummy_validator, - ) + metadata = create_validator_metadata() LegacyValidatorRegistry.register(metadata) assert len(LegacyValidatorRegistry._validators) == 1 assert "test_validator" in LegacyValidatorRegistry._validators - def test_get_validator(self) -> None: + def test_get_validator( + self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + ) -> None: """Test retrieving a validator by identifier""" - - def dummy_validator(data: dict) -> dict: - return data - - metadata = LegacyValidatorMetadata( - identifier="test_validator", - resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.field1"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", - description="Test validator", - validator_function=dummy_validator, - ) + metadata = create_validator_metadata() LegacyValidatorRegistry.register(metadata) @@ -121,31 +137,22 @@ def test_get_nonexistent_validator(self) -> None: retrieved = LegacyValidatorRegistry.get_validator("nonexistent") assert retrieved is None - def test_get_validators_for_resource(self) -> None: + def test_get_validators_for_resource( + self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + ) -> None: """Test retrieving validators for a specific resource type""" - - def dummy_validator(data: dict) -> dict: - return data - # Register validators for different resource types - metadata1 = LegacyValidatorMetadata( + metadata1 = create_validator_metadata( identifier="samplestore_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.field1"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", description="Sample store validator", - validator_function=dummy_validator, ) - metadata2 = LegacyValidatorMetadata( + metadata2 = create_validator_metadata( identifier="operation_validator", resource_type=CoreResourceKinds.OPERATION, deprecated_field_paths=["config.field2"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", description="Operation validator", - validator_function=dummy_validator, ) LegacyValidatorRegistry.register(metadata1) @@ -165,41 +172,28 @@ def dummy_validator(data: dict) -> dict: assert len(operation_validators) == 1 assert operation_validators[0].identifier == "operation_validator" - def test_find_validators_for_deprecated_field_paths(self) -> None: + def test_find_validators_for_deprecated_field_paths( + self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + ) -> None: """Test finding validators that handle specific field paths""" - - def dummy_validator(data: dict) -> dict: - return data - # Register validators with different field paths - metadata1 = LegacyValidatorMetadata( + metadata1 = create_validator_metadata( identifier="validator1", - resource_type=CoreResourceKinds.SAMPLESTORE, deprecated_field_paths=["config.field1", "config.field2"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", description="Validator 1", - validator_function=dummy_validator, ) - metadata2 = LegacyValidatorMetadata( + metadata2 = create_validator_metadata( identifier="validator2", - resource_type=CoreResourceKinds.SAMPLESTORE, deprecated_field_paths=["config.specification.field3"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", description="Validator 2", - validator_function=dummy_validator, ) - metadata3 = LegacyValidatorMetadata( + metadata3 = create_validator_metadata( identifier="validator3", resource_type=CoreResourceKinds.DISCOVERYSPACE, deprecated_field_paths=["config.properties"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", description="Validator 3", - validator_function=dummy_validator, ) LegacyValidatorRegistry.register(metadata1) @@ -248,30 +242,20 @@ def dummy_validator(data: dict) -> dict: assert len(validators) == 1 assert validators[0].identifier == "validator3" - def test_list_all(self) -> None: + def test_list_all( + self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + ) -> None: """Test listing all validators""" - - def dummy_validator(data: dict) -> dict: - return data - - metadata1 = LegacyValidatorMetadata( + metadata1 = create_validator_metadata( identifier="validator1", - resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.field1"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", description="Validator 1", - validator_function=dummy_validator, ) - metadata2 = LegacyValidatorMetadata( + metadata2 = create_validator_metadata( identifier="validator2", resource_type=CoreResourceKinds.OPERATION, deprecated_field_paths=["config.field2"], - deprecated_from_version="1.0.0", - removed_from_version="2.0.0", description="Validator 2", - validator_function=dummy_validator, ) LegacyValidatorRegistry.register(metadata1) From c8cdb92e24d2963174d8042e5be0fd955c163a76 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 15:03:34 +0000 Subject: [PATCH 40/55] refactor(tests): remove tests that aren't needed anymore We don't match on leaf nodes anymore Signed-off-by: Alessandro Pomponio --- tests/core/test_validator_scope_fixes.py | 367 ----------------------- 1 file changed, 367 deletions(-) delete mode 100644 tests/core/test_validator_scope_fixes.py diff --git a/tests/core/test_validator_scope_fixes.py b/tests/core/test_validator_scope_fixes.py deleted file mode 100644 index 225aa706b..000000000 --- a/tests/core/test_validator_scope_fixes.py +++ /dev/null @@ -1,367 +0,0 @@ -# Copyright IBM Corporation 2025, 2026 -# SPDX-License-Identifier: MIT - -"""Tests for Phase 1 validator scope fixes - verifying validators only operate on config level""" - -import importlib - -from orchestrator.core.legacy.registry import LegacyValidatorRegistry - - -class TestValidatorScopeFixes: - """Test that validators correctly operate only on config level after Phase 1 fixes""" - - def setup_method(self) -> None: - """Clear registry and reload validators before each test for isolation""" - # Clear the registry to ensure clean state - LegacyValidatorRegistry._validators = {} - # Reload all validator module files to re-register them - import orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore - import orchestrator.core.legacy.validators.discoveryspace.properties_field_removal - import orchestrator.core.legacy.validators.operation.actuators_field_removal - import orchestrator.core.legacy.validators.resource.entitysource_to_samplestore - import orchestrator.core.legacy.validators.samplestore.entitysource_migrations - import orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration - - importlib.reload( - orchestrator.core.legacy.validators.discoveryspace.properties_field_removal - ) - importlib.reload( - orchestrator.core.legacy.validators.discoveryspace.entitysource_to_samplestore - ) - importlib.reload( - orchestrator.core.legacy.validators.operation.actuators_field_removal - ) - importlib.reload( - orchestrator.core.legacy.validators.resource.entitysource_to_samplestore - ) - importlib.reload( - orchestrator.core.legacy.validators.samplestore.v1_to_v2_csv_migration - ) - importlib.reload( - orchestrator.core.legacy.validators.samplestore.entitysource_migrations - ) - - def test_discoveryspace_properties_removal_scope(self) -> None: - """Verify properties_field_removal only modifies config, not resource level""" - - # Test data with 'properties' at both levels - resource_data = { - "kind": "discoveryspace", - "identifier": "test-space", - "properties": "SHOULD_NOT_BE_REMOVED", # Resource level - "config": { - "properties": ["prop1", "prop2"], # Config level (should be removed) - "sampleStoreIdentifier": "store-1", - }, - } - - # Get validator - validator = LegacyValidatorRegistry.get_validator( - "discoveryspace_properties_field_removal" - ) - assert validator is not None - - # Apply validator - result = validator.validator_function(resource_data.copy()) - - # Verify: resource-level field unchanged, config-level field removed - assert "properties" in result # Resource level preserved - assert result["properties"] == "SHOULD_NOT_BE_REMOVED" - assert "properties" not in result["config"] # Config level removed - assert result["config"]["sampleStoreIdentifier"] == "store-1" - - def test_discoveryspace_entitysource_migration_scope(self) -> None: - """Verify entitysource_to_samplestore only modifies config, not resource level""" - - # Test data with entitySourceIdentifier at both levels - resource_data = { - "kind": "discoveryspace", - "identifier": "test-space", - "entitySourceIdentifier": "SHOULD_NOT_BE_REMOVED", # Resource level - "config": { - "entitySourceIdentifier": "old-source", # Config level (should migrate) - }, - } - - # Get validator - validator = LegacyValidatorRegistry.get_validator( - "discoveryspace_entitysource_to_samplestore" - ) - assert validator is not None - - # Apply validator - result = validator.validator_function(resource_data.copy()) - - # Verify: resource-level field unchanged, config-level field migrated - assert "entitySourceIdentifier" in result # Resource level preserved - assert result["entitySourceIdentifier"] == "SHOULD_NOT_BE_REMOVED" - assert "entitySourceIdentifier" not in result["config"] # Config level removed - assert result["config"]["sampleStoreIdentifier"] == "old-source" # Migrated - - def test_operation_actuators_removal_scope(self) -> None: - """Verify actuators_field_removal only modifies config, not resource level""" - - # Test data with actuators at both levels - resource_data = { - "kind": "operation", - "identifier": "test-op", - "actuators": "SHOULD_NOT_BE_REMOVED", # Resource level - "config": { - "actuators": ["act1", "act2"], # Config level (should be removed) - "operatorIdentifier": "op1", - }, - } - - # Get validator - validator = LegacyValidatorRegistry.get_validator( - "operation_actuators_field_removal" - ) - assert validator is not None - - # Apply validator - result = validator.validator_function(resource_data.copy()) - - # Verify: resource-level field unchanged, config-level field removed - assert "actuators" in result # Resource level preserved - assert result["actuators"] == "SHOULD_NOT_BE_REMOVED" - assert "actuators" not in result["config"] # Config level removed - assert result["config"]["operatorIdentifier"] == "op1" - - def test_samplestore_module_type_entitysource_migration_scope(self) -> None: - """Verify entitysource module type migration only modifies config, not resource level""" - - # Test data with moduleType at both levels - resource_data = { - "kind": "samplestore", - "type": "csv", - "identifier": "test-store", - "moduleType": "SHOULD_NOT_BE_REMOVED", # Resource level - "config": { - "moduleType": "entity_source", # Config level (should migrate) - }, - } - - # Get validator for module type entitysource migration - validator = LegacyValidatorRegistry.get_validator( - "samplestore_module_type_entitysource_to_samplestore" - ) - assert validator is not None - - # Apply validator - result = validator.validator_function(resource_data.copy()) - - # Verify: resource-level field unchanged, config-level field migrated - assert "moduleType" in result # Resource level preserved - assert result["moduleType"] == "SHOULD_NOT_BE_REMOVED" - assert result["config"]["moduleType"] == "sample_store" # Migrated - - def test_samplestore_csv_migration_scope(self) -> None: - """Verify CSV v1 to v2 migration only modifies config, not resource level""" - - # Test data with constitutivePropertyColumns at both levels - resource_data = { - "kind": "samplestore", - "type": "csv", - "identifier": "test-store", - "constitutivePropertyColumns": "SHOULD_NOT_BE_REMOVED", # Resource level - "config": { - "identifierColumn": "id", - "constitutivePropertyColumns": ["prop1", "prop2"], # Config (migrate) - "experiments": [ - { - "experimentIdentifier": "exp1", - "actuatorIdentifier": "act1", - "propertyMap": ["obs1", "obs2"], - } - ], - }, - } - - # Get validator - validator = LegacyValidatorRegistry.get_validator( - "csv_constitutive_columns_migration" - ) - assert validator is not None - - # Apply validator - result = validator.validator_function(resource_data.copy()) - - # Verify: resource-level field unchanged, config-level field migrated - assert "constitutivePropertyColumns" in result # Resource level preserved - assert result["constitutivePropertyColumns"] == "SHOULD_NOT_BE_REMOVED" - assert "constitutivePropertyColumns" not in result["config"] # Config removed - # Verify migration happened in config - exp = result["config"]["experiments"][0] - assert "propertyMap" not in exp - assert "observedPropertyMap" in exp - assert exp["observedPropertyMap"] == ["obs1", "obs2"] - assert "constitutivePropertyMap" in exp - assert exp["constitutivePropertyMap"] == ["prop1", "prop2"] - - def test_resource_kind_field_operates_at_resource_level(self) -> None: - """Verify resource-level validators (like kind migration) operate at resource level""" - - # Test data with entitysource kind at resource level - resource_data = { - "kind": "entitysource", # Resource level (should be migrated) - "type": "csv", - "identifier": "test-store", - "config": { - "identifierColumn": "id", - }, - } - - # Get validator - validator = LegacyValidatorRegistry.get_validator( - "samplestore_kind_entitysource_to_samplestore" - ) - assert validator is not None - - # Apply validator - result = validator.validator_function(resource_data.copy()) - - # Verify: resource-level kind field was migrated - assert result["kind"] == "samplestore" - assert result["type"] == "csv" - assert result["identifier"] == "test-store" - - def test_validators_preserve_unrelated_fields(self) -> None: - """Verify validators don't modify unrelated fields at any level""" - - # Test data with many fields - resource_data = { - "kind": "discoveryspace", - "identifier": "test-space", - "unrelated_resource_field": "preserve_me", - "config": { - "properties": ["prop1", "prop2"], # Will be removed - "sampleStoreIdentifier": "store-1", - "unrelated_config_field": "preserve_me_too", - "nested": { - "deep_field": "also_preserve", - }, - }, - } - - # Get validator - validator = LegacyValidatorRegistry.get_validator( - "discoveryspace_properties_field_removal" - ) - assert validator is not None - - # Apply validator - result = validator.validator_function(resource_data.copy()) - - # Verify: unrelated fields preserved at all levels - assert result["unrelated_resource_field"] == "preserve_me" - assert result["config"]["unrelated_config_field"] == "preserve_me_too" - assert result["config"]["nested"]["deep_field"] == "also_preserve" - assert result["config"]["sampleStoreIdentifier"] == "store-1" - # But deprecated field removed - assert "properties" not in result["config"] - - def test_validators_handle_missing_config_gracefully(self) -> None: - """Verify validators handle missing config field gracefully""" - - # Test data without config - resource_data = { - "kind": "discoveryspace", - "identifier": "test-space", - } - - # Get validator - validator = LegacyValidatorRegistry.get_validator( - "discoveryspace_properties_field_removal" - ) - assert validator is not None - - # Apply validator - should not crash - result = validator.validator_function(resource_data.copy()) - - # Verify: data unchanged - assert result == resource_data - - def test_validators_handle_empty_config_gracefully(self) -> None: - """Verify validators handle empty config dict gracefully""" - - # Test data with empty config - resource_data = { - "kind": "discoveryspace", - "identifier": "test-space", - "config": {}, - } - - # Get validator - validator = LegacyValidatorRegistry.get_validator( - "discoveryspace_properties_field_removal" - ) - assert validator is not None - - # Apply validator - should not crash - result = validator.validator_function(resource_data.copy()) - - # Verify: data unchanged - assert result == resource_data - - def test_samplestore_module_name_exact_match(self) -> None: - """Verify module name migration uses exact matching only""" - - # Get validator - validator = LegacyValidatorRegistry.get_validator( - "samplestore_module_name_entitysource_to_samplestore" - ) - assert validator is not None - - # Test data with module names that should NOT be migrated - # (not exact matches) - no_migration_cases = [ - "my_orchestrator.core.entitysource_wrapper", # Contains substring but not exact - "orchestrator.core.entitysource.csv", # Submodule, not exact match - "orchestrator.plugins.entitysources.custom", # Submodule, not exact match - ] - - for module_name in no_migration_cases: - data = { - "kind": "samplestore", - "type": "csv", - "identifier": "test-store", - "config": { - "moduleName": module_name, - }, - } - result = validator.validator_function(data.copy()) - assert result["config"]["moduleName"] == module_name, ( - f"Expected {module_name} to NOT be migrated, " - f"but got {result['config']['moduleName']}" - ) - - # Test exact matches that SHOULD migrate - exact_match_cases = [ - ( - "orchestrator.core.entitysource", - "orchestrator.core.samplestore", - ), - ( - "orchestrator.plugins.entitysources", - "orchestrator.plugins.samplestores", - ), - ] - - for old_name, expected_new_name in exact_match_cases: - data = { - "kind": "samplestore", - "type": "csv", - "identifier": "test-store", - "config": { - "moduleName": old_name, - }, - } - result = validator.validator_function(data.copy()) - assert result["config"]["moduleName"] == expected_new_name, ( - f"Expected {old_name} to migrate to {expected_new_name}, " - f"but got {result['config']['moduleName']}" - ) - - -# Made with Bob From 69cdbc87b25195645959d11a5c7d9c3e32bd7658 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 15:44:52 +0000 Subject: [PATCH 41/55] refactor: remove changes that aren't needed anymore Signed-off-by: Alessandro Pomponio --- website/docs/cli/legacy-validators.md | 393 -------------------------- website/mkdocs.yml | 27 +- 2 files changed, 13 insertions(+), 407 deletions(-) delete mode 100644 website/docs/cli/legacy-validators.md diff --git a/website/docs/cli/legacy-validators.md b/website/docs/cli/legacy-validators.md deleted file mode 100644 index 2f821b9c7..000000000 --- a/website/docs/cli/legacy-validators.md +++ /dev/null @@ -1,393 +0,0 @@ -# Legacy Validators - -Legacy validators help you upgrade old resource files that use deprecated fields -or formats. This guide covers the legacy validator system and its advanced -features. - -## Overview - -The legacy validator system provides: - -- **Automatic dependency resolution**: Validators can depend on other validators -- **Smart error messages**: Get helpful suggestions when validation fails -- **Progress tracking**: Visual feedback during upgrade operations -- **Automatic ordering**: Validators run in the correct order automatically -- **Dry-run mode**: Preview changes before applying them - -## Quick Start - -### List Available Validators - -View all legacy validators: - -```bash -ado legacy list -``` - -View validators for a specific resource type: - -```bash -ado legacy list samplestore -``` - -### Get Validator Information - -Get detailed information about a specific validator: - -```bash -ado legacy info discoveryspace_properties_field_removal -``` - -This shows: - -- Description of what the validator does -- Which deprecated fields it handles -- Version information -- Usage examples - -### Upgrade Resources - -#### Upgrade Resources in Metastore - -Apply a legacy validator to resources in your metastore: - -```bash -ado upgrade discoveryspace --apply-legacy-validator discoveryspace_properties_field_removal -``` - -Apply multiple validators (they will be automatically ordered): - -```bash -ado upgrade samplestore \ - --apply-legacy-validator samplestore_kind_entitysource_to_samplestore \ - --apply-legacy-validator samplestore_module_type_entitysource_to_samplestore -``` - -#### Upgrade Local YAML Files - -Upgrade local YAML files without loading them into the metastore: - -```bash -ado legacy upgrade --file my-resource.yaml \ - --apply-legacy-validator discoveryspace_properties_field_removal -``` - -Upgrade multiple files: - -```bash -ado legacy upgrade \ - --file resource1.yaml \ - --file resource2.yaml \ - --apply-legacy-validator validator_name -``` - -## Advanced Features - -### Automatic Dependency Resolution - -Validators can depend on other validators. When you specify a validator, the -system automatically: - -1. Includes all required dependencies -2. Orders validators correctly using topological sort -3. Notifies you when dependencies are auto-included - -**Example:** - -If validator B depends on validator A, you only need to specify B: - -```bash -ado upgrade samplestore --apply-legacy-validator validator_b -``` - -The system will automatically: - -- Include validator A -- Run A before B -- Show you: "Auto-included dependencies: validator_a" - -### Circular Dependency Detection - -The system detects circular dependencies and provides clear error messages: - -```text -Error: Circular dependency detected among validators: validator_a, validator_b -``` - -### Enhanced Error Messages - -When validation fails, you get: - -1. **Detailed field errors**: Exact fields that failed and why -2. **Applicable validators**: List of validators that can fix the issues -3. **Dependency information**: Which validators depend on others -4. **Ready-to-use commands**: Complete commands you can copy-paste - -**Example error output:** - -```text -Validation Error in discoveryspace 'my-space' - -Fields with validation errors: 2 field(s) - -Error details: - • config.properties: - - Extra inputs are not permitted - • config.entitySourceIdentifier: - - Field required - -Available legacy validators: - - 1. discoveryspace_properties_field_removal - Removes the deprecated 'properties' field - Handles: properties - Deprecated: v0.10.1 - - 2. discoveryspace_entitysource_to_samplestore - Renames 'entitySourceIdentifier' to 'sampleStoreIdentifier' - Handles: entitySourceIdentifier - Deprecated: v0.9.6 - Dependencies: discoveryspace_properties_field_removal - -To upgrade using legacy validators: - ado upgrade discoveryspace \ - --apply-legacy-validator discoveryspace_properties_field_removal \ - --apply-legacy-validator discoveryspace_entitysource_to_samplestore -``` - -### Progress Tracking - -When upgrading local files, you see: - -- Overall progress bar for files -- Per-file validator progress -- Real-time status updates -- Clear success/failure indicators - -**Example:** - -```text -Processing 3 file(s) with 2 validator(s)... - -⠋ Processing file1.yaml... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 33% - Applying validator_a... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 50% - -File: file1.yaml -Applied validators: validator_a, validator_b -✓ Upgraded: file1.yaml -``` - -### Dry-Run Mode - -Preview changes without modifying files: - -```bash -ado legacy upgrade --file my-resource.yaml \ - --apply-legacy-validator validator_name \ - --dry-run -``` - -This shows: - -- Which validators would be applied -- Preview of the modified YAML -- No files are actually changed - -### Backup Creation - -When upgrading files in-place, backups are created automatically: - -```bash -ado legacy upgrade --file my-resource.yaml \ - --apply-legacy-validator validator_name -``` - -Creates: `my-resource.yaml.bak` - -Disable backups: - -```bash -ado legacy upgrade --file my-resource.yaml \ - --apply-legacy-validator validator_name \ - --no-backup -``` - -### Output to Different Directory - -Upgrade files to a different directory: - -```bash -ado legacy upgrade \ - --file old/resource.yaml \ - --apply-legacy-validator validator_name \ - --output-dir upgraded/ -``` - -## Available Validators - -### Discovery Space Validators - -#### discoveryspace_properties_field_removal - -- **Handles**: `properties` -- **Description**: Removes the deprecated 'properties' field -- **Deprecated from**: v0.10.1 -- **Removed from**: v1.0.0 - -#### discoveryspace_entitysource_to_samplestore - -- **Handles**: `entitySourceIdentifier` -- **Description**: Renames 'entitySourceIdentifier' to 'sampleStoreIdentifier' -- **Deprecated from**: v0.9.6 -- **Removed from**: v1.0.0 - -### Operation Validators - -#### operation_actuators_field_removal - -- **Handles**: `actuators` -- **Description**: Removes the deprecated 'actuators' field -- **Deprecated from**: v0.9.6 -- **Removed from**: v1.0.0 -- **See**: - [Operation Configuration](../resources/operation.md#the-operation-configuration-yaml) - -#### randomwalk_mode_to_sampler_config - -- **Handles**: `mode`, `grouping`, `samplerType` -- **Description**: Migrates random_walk parameters to nested 'samplerConfig' -- **Deprecated from**: v1.0.1 -- **Removed from**: v1.2 -- **See**: - [Random Walk Configuration](../operators/random-walk.md#configuring-a-randomwalk) - -### Sample Store Validators - -#### samplestore_kind_entitysource_to_samplestore - -- **Handles**: `kind` -- **Description**: Converts resource kind from 'entitysource' to 'samplestore' -- **Deprecated from**: v0.9.6 -- **Removed from**: v1.0.0 - -#### samplestore_module_type_entitysource_to_samplestore - -- **Handles**: `moduleType` -- **Description**: Converts moduleType from 'entity_source' to 'sample_store' -- **Deprecated from**: v0.9.6 -- **Removed from**: v1.0.0 - -#### samplestore_module_class_entitysource_to_samplestore - -- **Handles**: `moduleClass` -- **Description**: Converts moduleClass from EntitySource to SampleStore naming -- **Deprecated from**: v0.9.6 -- **Removed from**: v1.0.0 - -#### samplestore_module_name_entitysource_to_samplestore - -- **Handles**: `moduleName` -- **Description**: Updates module paths from entitysource to samplestore -- **Deprecated from**: v0.9.6 -- **Removed from**: v1.0.0 - -#### csv_constitutive_columns_migration - -- **Handles**: `constitutivePropertyColumns`, `propertyMap` -- **Description**: Migrates CSV sample stores from v1 to v2 format -- **Deprecated from**: v1.3.5 -- **Removed from**: v1.6.0 - -## Best Practices - -### 1. Use `ado legacy list` First - -Before upgrading, check which validators are available: - -```bash -ado legacy list samplestore -``` - -### 2. Get Detailed Information - -Use `ado legacy info` to understand what a validator does: - -```bash -ado legacy info csv_constitutive_columns_migration -``` - -### 3. Test with Dry-Run - -Always test with `--dry-run` first: - -```bash -ado legacy upgrade --file my-resource.yaml \ - --apply-legacy-validator validator_name \ - --dry-run -``` - -### 4. Let Dependencies Auto-Resolve - -Don't manually specify dependencies - the system handles it: - -```bash -# Good - just specify what you need -ado upgrade samplestore --apply-legacy-validator validator_b - -# Unnecessary - dependencies are automatic -ado upgrade samplestore \ - --apply-legacy-validator validator_a \ - --apply-legacy-validator validator_b -``` - -### 5. Keep Backups - -When upgrading important files, keep the default backup behavior: - -```bash -# Backups created automatically -ado legacy upgrade --file important.yaml \ - --apply-legacy-validator validator_name -``` - -## Troubleshooting - -### Validator Not Found - -If you get "Unknown legacy validator": - -1. Check the validator name: `ado legacy list` -2. Ensure you're using the full identifier -3. Check for typos - -### Circular Dependency Error - -If you see "Circular dependency detected": - -1. This indicates a bug in validator definitions -2. Report the issue with the validator names -3. Use validators individually as a workaround - -### Missing Dependencies - -If you see "Missing validator dependencies": - -1. The validator depends on another validator that doesn't exist -2. This indicates a configuration issue -3. Report the issue with details - -### Validation Still Fails - -If validation fails after applying validators: - -1. Check if you applied all suggested validators -2. Verify the resource format matches expectations -3. Check for additional deprecated fields -4. Use `ado legacy list` to find other applicable validators - -## See Also - -- [Resource Upgrade Command](../cli/upgrade.md) -- [Discovery Space Resources](../resources/discoveryspace.md) -- [Sample Store Resources](../resources/samplestore.md) -- [Operation Resources](../resources/operation.md) diff --git a/website/mkdocs.yml b/website/mkdocs.yml index 0676e4c71..dc256524d 100644 --- a/website/mkdocs.yml +++ b/website/mkdocs.yml @@ -4,15 +4,15 @@ site_url: "https://ibm.github.io/ado" site_description: "Documentation website for https://github.com/ibm/ado" site_author: "The ado authors" # Repository Information -repo_name: "ibm/ado" -repo_url: "https://github.com/ibm/ado" -edit_uri: "edit/main/website/docs" -docs_dir: "docs" -site_dir: "site" -remote_branch: "gh-pages" -remote_name: "origin" +repo_name: 'ibm/ado' +repo_url: 'https://github.com/ibm/ado' +edit_uri: 'edit/main/website/docs' +docs_dir: 'docs' +site_dir: 'site' +remote_branch: 'gh-pages' +remote_name: 'origin' # Server info -dev_addr: "127.0.0.1:8000" +dev_addr: '127.0.0.1:8000' # Copyright copyright: Copyright IBM Research. # Extra links as icons at the bottom of the page @@ -31,8 +31,8 @@ theme: include_search_page: true search_index_only: true custom_dir: theme - favicon: "favicon.ico" - logo: "logo-ibm.png" + favicon: 'favicon.ico' + logo: 'logo-ibm.png' font: false palette: # Palette toggle for automatic mode @@ -78,7 +78,7 @@ theme: - search.suggest - toc.integrate # Integrate page TOC in left sidebar - wider page icon: - edit: "material/pencil-outline" + edit: 'material/pencil-outline' markdown_extensions: admonition: {} # def_list: {} @@ -95,6 +95,7 @@ markdown_extensions: pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg + # Better list support: https://facelessuser.github.io/pymdown-extensions/extensions/fancylists/ pymdownx.fancylists: {} # Code syntax highlighting: https://facelessuser.github.io/pymdown-extensions/extensions/highlight/ pymdownx.highlight: {} @@ -109,7 +110,7 @@ markdown_extensions: # Strip HTML: https://facelessuser.github.io/pymdown-extensions/extensions/striphtml/ pymdownx.striphtml: {} # Better fencing: https://facelessuser.github.io/pymdown-extensions/extensions/superfences/ - pymdownx.superfences: # Better list support: https://facelessuser.github.io/pymdown-extensions/extensions/fancylists/ + pymdownx.superfences: preserve_tabs: true # Tabs: https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ pymdownx.tabbed: @@ -143,8 +144,6 @@ nav: - Developing ado: getting-started/developing.md - Roadmap: getting-started/roadmap.md - Contributing: getting-started/contributing.md - - CLI Reference: - - Legacy Validators: cli/legacy-validators.md - Examples: - examples/examples.md - Taking a random walk: examples/random-walk.md From c168a9964edabb248a65c22c802be31b78dcaacd Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 15:47:29 +0000 Subject: [PATCH 42/55] refactor(test): avoid mocks Signed-off-by: Alessandro Pomponio --- tests/core/test_upgrade_transaction_safety.py | 431 ++++++++++-------- 1 file changed, 230 insertions(+), 201 deletions(-) diff --git a/tests/core/test_upgrade_transaction_safety.py b/tests/core/test_upgrade_transaction_safety.py index 8fbeedd50..e8aee6fc7 100644 --- a/tests/core/test_upgrade_transaction_safety.py +++ b/tests/core/test_upgrade_transaction_safety.py @@ -1,15 +1,27 @@ # Copyright IBM Corporation 2025, 2026 # SPDX-License-Identifier: MIT -"""Tests for Phase 1 transaction safety in upgrade handler""" +"""Integration tests for Phase 1 transaction safety in upgrade handler""" -from unittest.mock import MagicMock, patch +import json import pytest +import sqlalchemy import typer +from orchestrator.cli.core.config import AdoConfiguration +from orchestrator.cli.models.parameters import AdoUpgradeCommandParameters +from orchestrator.cli.utils.generic.wrappers import get_sql_store +from orchestrator.cli.utils.resources.handlers import handle_ado_upgrade from orchestrator.core.legacy.registry import LegacyValidatorRegistry, legacy_validator from orchestrator.core.resources import CoreResourceKinds +from orchestrator.core.samplestore.config import ( + SampleStoreConfiguration, + SampleStoreModuleConf, + SampleStoreSpecification, +) +from orchestrator.core.samplestore.resource import SampleStoreResource +from orchestrator.metastore.project import ProjectContext class TestUpgradeTransactionSafety: @@ -19,214 +31,243 @@ def setup_method(self) -> None: """Clear the registry before each test""" LegacyValidatorRegistry._validators = {} - def test_all_resources_validated_before_any_saved(self) -> None: + @pytest.mark.parametrize("valid_ado_project_context", ["sqlite"], indirect=True) + def test_all_resources_validated_before_any_saved( + self, + valid_ado_project_context: ProjectContext, + ) -> None: """Test that all resources are validated before any are saved""" - # Register a test validator + # Register a test validator that transforms old_field -> new_field @legacy_validator( identifier="test_transaction_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.metadata.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test transaction validator", ) def test_validator(data: dict) -> dict: - if "config" in data and "old_field" in data["config"]: - data["config"]["new_field"] = data["config"].pop("old_field") + if "config" in data and "metadata" in data["config"]: + metadata = data["config"]["metadata"] + if "old_field" in metadata: + metadata["new_field"] = metadata.pop("old_field") return data - # Create mock resources - mock_resource1 = MagicMock() - mock_resource1.model_dump.return_value = { - "kind": "samplestore", - "identifier": "res1", - "config": {"old_field": "value1"}, - } - - mock_resource2 = MagicMock() - mock_resource2.model_dump.return_value = { - "kind": "samplestore", - "identifier": "res2", - "config": {"old_field": "value2"}, - } - - # Mock resource class - mock_resource_class = MagicMock() - validated_resources = [] - - def mock_validate(data: dict) -> MagicMock: - validated = MagicMock() - validated.model_dump.return_value = data - validated_resources.append(data["identifier"]) - return validated - - mock_resource_class.model_validate.side_effect = mock_validate - - # Mock SQL store - mock_sql_store = MagicMock() - mock_sql_store.getResourcesOfKind.return_value = { - "res1": mock_resource1, - "res2": mock_resource2, - } - mock_sql_store.getResourceIdentifiersOfKind.return_value = { - "IDENTIFIER": ["res1", "res2"] - } - mock_sql_store.getResourceRaw.side_effect = lambda id: ( - mock_resource1.model_dump() if id == "res1" else mock_resource2.model_dump() + # Create two sample store resources with old_field in metadata + resource1 = SampleStoreResource( + identifier="test_res1", + config=SampleStoreConfiguration( + specification=SampleStoreSpecification( + module=SampleStoreModuleConf( + moduleClass="SQLSampleStore", + moduleName="orchestrator.core.samplestore.sql", + ), + storageLocation=valid_ado_project_context.metadataStore, + ), + metadata={"old_field": "value1"}, + ), ) - update_calls = [] - - def track_update(resource: MagicMock) -> None: - update_calls.append(resource.model_dump()["identifier"]) - - mock_sql_store.updateResource.side_effect = track_update - - # Mock parameters - mock_params = MagicMock() - mock_params.apply_legacy_validator = ["test_transaction_validator"] - mock_params.list_legacy_validators = False - mock_params.ado_configuration.project_context = "test_context" - - # Patch dependencies - with ( - patch( - "orchestrator.cli.utils.resources.handlers.get_sql_store", - return_value=mock_sql_store, - ), - patch( - "orchestrator.core.kindmap", - {"samplestore": mock_resource_class}, + resource2 = SampleStoreResource( + identifier="test_res2", + config=SampleStoreConfiguration( + specification=SampleStoreSpecification( + module=SampleStoreModuleConf( + moduleClass="SQLSampleStore", + moduleName="orchestrator.core.samplestore.sql", + ), + storageLocation=valid_ado_project_context.metadataStore, + ), + metadata={"old_field": "value2"}, ), - patch("orchestrator.cli.utils.resources.handlers.Status"), - patch("orchestrator.cli.utils.resources.handlers.console_print"), - ): - from orchestrator.cli.utils.resources.handlers import handle_ado_upgrade - - # Call the upgrade handler - handle_ado_upgrade( - parameters=mock_params, - resource_type=CoreResourceKinds.SAMPLESTORE, - ) + ) - # Verify: all resources validated before any saved - # Both resources should be validated - assert len(validated_resources) == 2 - assert "res1" in validated_resources - assert "res2" in validated_resources + # Save resources to database + sql_store = get_sql_store(project_context=valid_ado_project_context) + sql_store.updateResource(resource=resource1) + sql_store.updateResource(resource=resource2) + + # Now manually add the deprecated field to the raw data in the database + # We need to update the JSON directly in the database + with sql_store.engine.begin() as conn: + # Get current data + raw1 = sql_store.getResourceRaw("test_res1") + raw1["config"]["metadata"]["old_field"] = "value1" + + # Update in database + update_stmt = sqlalchemy.text( + "UPDATE resources SET data = :data WHERE identifier = :identifier" + ).bindparams(data=json.dumps(raw1), identifier="test_res1") + conn.execute(update_stmt) + + # Same for resource2 + raw2 = sql_store.getResourceRaw("test_res2") + raw2["config"]["metadata"]["old_field"] = "value2" + + update_stmt = sqlalchemy.text( + "UPDATE resources SET data = :data WHERE identifier = :identifier" + ).bindparams(data=json.dumps(raw2), identifier="test_res2") + conn.execute(update_stmt) + + # Create parameters for upgrade + ado_config = AdoConfiguration() + ado_config._project_context = valid_ado_project_context + params = AdoUpgradeCommandParameters( + ado_configuration=ado_config, + apply_legacy_validator=["test_transaction_validator"], + list_legacy_validators=False, + ) - # Both resources should be saved - assert len(update_calls) == 2 - assert "res1" in update_calls - assert "res2" in update_calls + # Call the upgrade handler + handle_ado_upgrade( + parameters=params, + resource_type=CoreResourceKinds.SAMPLESTORE, + ) - def test_validation_failure_prevents_all_saves(self) -> None: + # Verify both resources were upgraded + upgraded_res1 = sql_store.getResourceRaw("test_res1") + upgraded_res2 = sql_store.getResourceRaw("test_res2") + + assert upgraded_res1 is not None + assert upgraded_res2 is not None + assert "new_field" in upgraded_res1["config"]["metadata"] + assert "new_field" in upgraded_res2["config"]["metadata"] + assert upgraded_res1["config"]["metadata"]["new_field"] == "value1" + assert upgraded_res2["config"]["metadata"]["new_field"] == "value2" + assert "old_field" not in upgraded_res1["config"]["metadata"] + assert "old_field" not in upgraded_res2["config"]["metadata"] + + @pytest.mark.parametrize("valid_ado_project_context", ["sqlite"], indirect=True) + def test_validation_failure_prevents_all_saves( + self, + valid_ado_project_context: ProjectContext, + ) -> None: """Test that if any validation fails, no resources are saved""" - # Register a test validator + # Register a validator that will cause validation failure @legacy_validator( identifier="test_failing_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.metadata.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test failing validator", ) def test_validator(data: dict) -> dict: - if "config" in data and "old_field" in data["config"]: - data["config"]["new_field"] = data["config"].pop("old_field") + # Transform the field + if "config" in data and "metadata" in data["config"]: + metadata = data["config"]["metadata"] + if "old_field" in metadata: + metadata["new_field"] = metadata.pop("old_field") + + # Introduce an invalid field that will fail pydantic validation + # for the second resource only + if data.get("identifier") == "test_res2": + data["config"]["invalid_field_that_breaks_validation"] = "bad_value" + return data - # Create mock resources - one valid, one will fail validation - mock_resource1 = MagicMock() - mock_resource1.model_dump.return_value = { - "kind": "samplestore", - "identifier": "res1", - "config": {"old_field": "value1"}, - } - - mock_resource2 = MagicMock() - mock_resource2.model_dump.return_value = { - "kind": "samplestore", - "identifier": "res2", - "config": {"old_field": "value2"}, - } - - # Mock resource class - second validation fails - mock_resource_class = MagicMock() - validation_count = [0] - - def mock_validate(data: dict) -> MagicMock: - validation_count[0] += 1 - if validation_count[0] == 2: - # Second validation fails - raise a simple ValueError - raise ValueError("Validation failed for resource res2") - validated = MagicMock() - validated.model_dump.return_value = data - return validated - - mock_resource_class.model_validate.side_effect = mock_validate - - # Mock SQL store - mock_sql_store = MagicMock() - mock_sql_store.getResourcesOfKind.return_value = { - "res1": mock_resource1, - "res2": mock_resource2, - } - mock_sql_store.getResourceIdentifiersOfKind.return_value = { - "IDENTIFIER": ["res1", "res2"] - } - mock_sql_store.getResourceRaw.side_effect = lambda id: ( - mock_resource1.model_dump() if id == "res1" else mock_resource2.model_dump() + # Create two sample store resources + resource1 = SampleStoreResource( + identifier="test_res1", + config=SampleStoreConfiguration( + specification=SampleStoreSpecification( + module=SampleStoreModuleConf( + moduleClass="SQLSampleStore", + moduleName="orchestrator.core.samplestore.sql", + ), + storageLocation=valid_ado_project_context.metadataStore, + ), + metadata={"old_field": "value1"}, + ), ) - # Mock parameters - mock_params = MagicMock() - mock_params.apply_legacy_validator = ["test_failing_validator"] - mock_params.list_legacy_validators = False - mock_params.ado_configuration.project_context = "test_context" - - # Patch dependencies - with ( - patch( - "orchestrator.cli.utils.resources.handlers.get_sql_store", - return_value=mock_sql_store, + resource2 = SampleStoreResource( + identifier="test_res2", + config=SampleStoreConfiguration( + specification=SampleStoreSpecification( + module=SampleStoreModuleConf( + moduleClass="SQLSampleStore", + moduleName="orchestrator.core.samplestore.sql", + ), + storageLocation=valid_ado_project_context.metadataStore, + ), + metadata={"old_field": "value2"}, ), - patch( - "orchestrator.core.kindmap", - {"samplestore": mock_resource_class}, - ), - patch("orchestrator.cli.utils.resources.handlers.Status"), - patch( - "orchestrator.cli.utils.resources.handlers.console_print" - ) as mock_print, - ): - from orchestrator.cli.utils.resources.handlers import handle_ado_upgrade - - # Should raise typer.Exit due to validation failure - with pytest.raises(typer.Exit) as exc_info: - handle_ado_upgrade( - parameters=mock_params, - resource_type=CoreResourceKinds.SAMPLESTORE, - ) - - assert exc_info.value.exit_code == 1 + ) - # Verify: NO resources were saved (transaction safety) - mock_sql_store.updateResource.assert_not_called() + # Save resources to database + sql_store = get_sql_store(project_context=valid_ado_project_context) + sql_store.updateResource(resource=resource1) + sql_store.updateResource(resource=resource2) + + # Now manually add the deprecated field to the raw data in the database + with sql_store.engine.begin() as conn: + # Get current data + raw1 = sql_store.getResourceRaw("test_res1") + raw1["config"]["metadata"]["old_field"] = "value1" + + # Update in database + update_stmt = sqlalchemy.text( + "UPDATE resources SET data = :data WHERE identifier = :identifier" + ).bindparams(data=json.dumps(raw1), identifier="test_res1") + conn.execute(update_stmt) + + # Same for resource2 + raw2 = sql_store.getResourceRaw("test_res2") + raw2["config"]["metadata"]["old_field"] = "value2" + + update_stmt = sqlalchemy.text( + "UPDATE resources SET data = :data WHERE identifier = :identifier" + ).bindparams(data=json.dumps(raw2), identifier="test_res2") + conn.execute(update_stmt) + + # Store original data for comparison + original_res1 = sql_store.getResourceRaw("test_res1") + original_res2 = sql_store.getResourceRaw("test_res2") + + # Create parameters for upgrade + ado_config = AdoConfiguration() + ado_config._project_context = valid_ado_project_context + params = AdoUpgradeCommandParameters( + ado_configuration=ado_config, + apply_legacy_validator=["test_failing_validator"], + list_legacy_validators=False, + ) - # Verify error was printed - mock_print.assert_called() + # Should raise typer.Exit due to validation failure + with pytest.raises(typer.Exit) as exc_info: + handle_ado_upgrade( + parameters=params, + resource_type=CoreResourceKinds.SAMPLESTORE, + ) - def test_empty_resource_list_handled_gracefully(self) -> None: + assert exc_info.value.exit_code == 1 + + # Verify: NO resources were saved (transaction safety) + # Both resources should still have their original data + current_res1 = sql_store.getResourceRaw("test_res1") + current_res2 = sql_store.getResourceRaw("test_res2") + + assert current_res1 == original_res1 + assert current_res2 == original_res2 + assert "old_field" in current_res1["config"]["metadata"] + assert "old_field" in current_res2["config"]["metadata"] + assert "new_field" not in current_res1["config"]["metadata"] + assert "new_field" not in current_res2["config"]["metadata"] + + def test_empty_resource_list_handled_gracefully( + self, + valid_ado_project_context: ProjectContext, + ) -> None: """Test that empty resource list is handled without errors""" # Register a test validator @legacy_validator( identifier="test_empty_validator", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.old_field"], + deprecated_field_paths=["config.metadata.old_field"], deprecated_from_version="1.0.0", removed_from_version="2.0.0", description="Test empty validator", @@ -234,41 +275,29 @@ def test_empty_resource_list_handled_gracefully(self) -> None: def test_validator(data: dict) -> dict: return data - # Mock SQL store with no resources - mock_sql_store = MagicMock() - mock_sql_store.getResourcesOfKind.return_value = {} - mock_sql_store.getResourceIdentifiersOfKind.return_value = {"IDENTIFIER": []} - - # Mock parameters - mock_params = MagicMock() - mock_params.apply_legacy_validator = ["test_empty_validator"] - mock_params.list_legacy_validators = False - mock_params.ado_configuration.project_context = "test_context" - - # Patch dependencies - with ( - patch( - "orchestrator.cli.utils.resources.handlers.get_sql_store", - return_value=mock_sql_store, - ), - patch("orchestrator.cli.utils.resources.handlers.Status"), - patch( - "orchestrator.cli.utils.resources.handlers.console_print" - ) as mock_print, - ): - from orchestrator.cli.utils.resources.handlers import handle_ado_upgrade - - # Should complete without error - handle_ado_upgrade( - parameters=mock_params, - resource_type=CoreResourceKinds.SAMPLESTORE, - ) + # Don't create any resources - database starts empty for this test - # Verify: no updates attempted - mock_sql_store.updateResource.assert_not_called() + # Create parameters for upgrade + ado_config = AdoConfiguration() + ado_config._project_context = valid_ado_project_context + params = AdoUpgradeCommandParameters( + ado_configuration=ado_config, + apply_legacy_validator=["test_empty_validator"], + list_legacy_validators=False, + ) - # Verify message printed - mock_print.assert_called() + # Should complete without error + handle_ado_upgrade( + parameters=params, + resource_type=CoreResourceKinds.SAMPLESTORE, + ) + + # Verify no samplestore resources exist + sql_store = get_sql_store(project_context=valid_ado_project_context) + resources = sql_store.getResourcesOfKind( + kind=CoreResourceKinds.SAMPLESTORE.value + ) + assert len(resources) == 0 # Made with Bob From 4919a639e3ee13981dd8bb2f8c9b332d4c14ddc4 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 15:55:54 +0000 Subject: [PATCH 43/55] test: add isolated legacy validator registry fixtures Signed-off-by: Alessandro Pomponio --- tests/conftest.py | 64 +++++++++++++++++++ tests/core/test_legacy_registry.py | 40 +++++++----- tests/core/test_legacy_validators.py | 58 +++++------------ tests/core/test_upgrade_transaction_safety.py | 7 +- tests/core/test_validator_dependencies.py | 30 ++++----- 5 files changed, 121 insertions(+), 78 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 124323d5a..f2e91ee2e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,8 @@ import pytest import ray +from orchestrator.core.legacy.registry import LegacyValidatorRegistry + from .fixtures.core.datacontainer import * from .fixtures.core.samplestore import * from .fixtures.core.generators import * @@ -51,3 +53,65 @@ def initialize_ray() -> Generator[None, None, None]: ) yield ray.shutdown() + + +@pytest.fixture +def isolated_legacy_validator_registry() -> Generator[None, None, None]: + """Isolate the LegacyValidatorRegistry for each test. + + This fixture ensures that modifications to the registry in one test + do not affect other tests, even when running with pytest -n auto. + + The fixture: + 1. Saves the current registry state before the test + 2. Clears the registry for the test + 3. Restores the original state after the test + + Usage: + def test_something(isolated_legacy_validator_registry): + # Registry starts empty + # Register validators as needed for this test + # Changes won't affect other tests + """ + # Save the current state + original_validators = LegacyValidatorRegistry._validators.copy() + + # Clear for this test + LegacyValidatorRegistry._validators.clear() + + try: + yield + finally: + # Restore original state + LegacyValidatorRegistry._validators = original_validators + + +@pytest.fixture +def legacy_validators_loaded() -> Generator[None, None, None]: + """Ensure legacy validators are loaded and isolated for the test. + + This fixture: + 1. Imports the validators module to trigger registration + 2. Saves a copy of the registered validators + 3. Provides isolation so test modifications don't affect other tests + 4. Restores the validators after the test + + Use this when your test needs the actual validators to be registered + (e.g., for integration tests that use real validators). + + Usage: + def test_with_real_validators(legacy_validators_loaded): + # All validators are registered and available + # Test can use them without affecting other tests + """ + # Import to trigger registration (safe - only runs once per process) + import orchestrator.core.legacy.validators # noqa: F401 + + # Save the current state (includes all registered validators) + original_validators = LegacyValidatorRegistry._validators.copy() + + try: + yield + finally: + # Restore to ensure other tests see the same state + LegacyValidatorRegistry._validators = original_validators diff --git a/tests/core/test_legacy_registry.py b/tests/core/test_legacy_registry.py index dc1d569aa..4afaeaac6 100644 --- a/tests/core/test_legacy_registry.py +++ b/tests/core/test_legacy_registry.py @@ -105,12 +105,10 @@ def test_metadata_serialization( class TestLegacyValidatorRegistry: """Test the LegacyValidatorRegistry class""" - def setup_method(self) -> None: - """Clear the registry before each test""" - LegacyValidatorRegistry._validators = {} - def test_register_validator( - self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + self, + isolated_legacy_validator_registry: None, + create_validator_metadata: Callable[..., LegacyValidatorMetadata], ) -> None: """Test registering a validator""" metadata = create_validator_metadata() @@ -121,7 +119,9 @@ def test_register_validator( assert "test_validator" in LegacyValidatorRegistry._validators def test_get_validator( - self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + self, + isolated_legacy_validator_registry: None, + create_validator_metadata: Callable[..., LegacyValidatorMetadata], ) -> None: """Test retrieving a validator by identifier""" metadata = create_validator_metadata() @@ -132,13 +132,17 @@ def test_get_validator( assert retrieved is not None assert retrieved.identifier == "test_validator" - def test_get_nonexistent_validator(self) -> None: + def test_get_nonexistent_validator( + self, isolated_legacy_validator_registry: None + ) -> None: """Test retrieving a validator that doesn't exist""" retrieved = LegacyValidatorRegistry.get_validator("nonexistent") assert retrieved is None def test_get_validators_for_resource( - self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + self, + isolated_legacy_validator_registry: None, + create_validator_metadata: Callable[..., LegacyValidatorMetadata], ) -> None: """Test retrieving validators for a specific resource type""" # Register validators for different resource types @@ -173,7 +177,9 @@ def test_get_validators_for_resource( assert operation_validators[0].identifier == "operation_validator" def test_find_validators_for_deprecated_field_paths( - self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + self, + isolated_legacy_validator_registry: None, + create_validator_metadata: Callable[..., LegacyValidatorMetadata], ) -> None: """Test finding validators that handle specific field paths""" # Register validators with different field paths @@ -243,7 +249,9 @@ def test_find_validators_for_deprecated_field_paths( assert validators[0].identifier == "validator3" def test_list_all( - self, create_validator_metadata: Callable[..., LegacyValidatorMetadata] + self, + isolated_legacy_validator_registry: None, + create_validator_metadata: Callable[..., LegacyValidatorMetadata], ) -> None: """Test listing all validators""" metadata1 = create_validator_metadata( @@ -324,11 +332,9 @@ def test_field_path_matching_with_real_validators(self) -> None: class TestLegacyValidatorDecorator: """Test the @legacy_validator decorator""" - def setup_method(self) -> None: - """Clear the registry before each test""" - LegacyValidatorRegistry._validators = {} - - def test_decorator_registers_validator(self) -> None: + def test_decorator_registers_validator( + self, isolated_legacy_validator_registry: None + ) -> None: """Test that the decorator registers the validator""" @legacy_validator( @@ -351,7 +357,9 @@ def my_validator(data: dict) -> dict: result = my_validator(test_data) assert result == test_data - def test_decorator_preserves_function_metadata(self) -> None: + def test_decorator_preserves_function_metadata( + self, isolated_legacy_validator_registry: None + ) -> None: """Test that the decorator preserves function metadata""" @legacy_validator( diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index 86aa35d67..c7ed625a9 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -16,31 +16,9 @@ class TestLegacyValidatorWithPydantic: """Test legacy validators working with pydantic models""" - def setup_method(self) -> None: - """Setup method - ensure validators are available before each test""" - # Always import validators to ensure they're registered - # This is safe because Python only executes module-level code once - import orchestrator.core.legacy.validators # noqa: F401 - - # If validators have been cleared by previous tests, we need to restore them - # Since Python won't re-execute the @legacy_validator decorators on re-import, - # we need to save a copy on first access and restore it when needed - if not hasattr(self.__class__, "_initial_validators"): - # First time - save the validators - self.__class__._initial_validators = ( - LegacyValidatorRegistry._validators.copy() - ) - elif ( - not LegacyValidatorRegistry._validators - or "csv_constitutive_columns_migration" - not in LegacyValidatorRegistry._validators - ): - # Validators were cleared - restore them - LegacyValidatorRegistry._validators = ( - self.__class__._initial_validators.copy() - ) - - def test_validator_applied_during_model_validation(self) -> None: + def test_validator_applied_during_model_validation( + self, isolated_legacy_validator_registry: None + ) -> None: """Test that a legacy validator can be manually applied before pydantic validation""" # Define a simple pydantic model @@ -75,7 +53,9 @@ def migrate_old_to_new(data: dict) -> dict: model = OldModel.model_validate(migrated_data) assert model.new_field == "test_value" - def test_csv_sample_store_migration_validator(self) -> None: + def test_csv_sample_store_migration_validator( + self, legacy_validators_loaded: None + ) -> None: """Test the CSV sample store migration validator with realistic data""" # Get the validator (should be registered from setup_method) @@ -116,7 +96,9 @@ def test_csv_sample_store_migration_validator(self) -> None: assert "constitutivePropertyMap" in exp assert exp["constitutivePropertyMap"] == ["prop1", "prop2"] - def test_entitysource_to_samplestore_migration(self) -> None: + def test_entitysource_to_samplestore_migration( + self, legacy_validators_loaded: None + ) -> None: """Test the entitysource to samplestore kind migration""" # Import the validator to register it @@ -146,7 +128,7 @@ def test_entitysource_to_samplestore_migration(self) -> None: assert migrated_data["type"] == "csv" assert migrated_data["identifier"] == "test_store" - def test_chained_validators(self) -> None: + def test_chained_validators(self, isolated_legacy_validator_registry: None) -> None: """Test applying multiple validators in sequence""" # Register two validators @@ -198,21 +180,9 @@ def step2(data: dict) -> dict: class TestUpgradeHandlerIntegration: """Integration tests for ado upgrade with legacy validators via CLI""" - def setup_method(self) -> None: - """Setup - ensure validators are registered""" - import orchestrator.core.legacy.validators # noqa: F401 - - if not hasattr(self.__class__, "_initial_validators"): - self.__class__._initial_validators = ( - LegacyValidatorRegistry._validators.copy() - ) - elif not LegacyValidatorRegistry._validators: - LegacyValidatorRegistry._validators = ( - self.__class__._initial_validators.copy() - ) - def test_upgrade_applies_legacy_validator_via_cli( self, + legacy_validators_loaded: None, tmp_path: Path, valid_ado_project_context: ProjectContext, create_active_ado_context: Callable, @@ -291,6 +261,7 @@ def test_validator(data: dict) -> dict: def test_upgrade_rejects_mismatched_validator_type( self, + legacy_validators_loaded: None, tmp_path: Path, valid_ado_project_context: ProjectContext, create_active_ado_context: Callable, @@ -378,6 +349,7 @@ def test_upgrade_rejects_unknown_validator( def test_upgrade_auto_resolves_validator_dependencies( self, + legacy_validators_loaded: None, tmp_path: Path, valid_ado_project_context: ProjectContext, create_active_ado_context: Callable, @@ -514,7 +486,9 @@ def selective(data: dict) -> dict: assert result["keep_field3"] == ["list", "of", "items"] assert result["keep_field4"] == {"nested": "dict"} - def test_validator_handles_missing_fields_gracefully(self) -> None: + def test_validator_handles_missing_fields_gracefully( + self, isolated_legacy_validator_registry: None + ) -> None: """Test that validators handle missing deprecated fields gracefully""" @legacy_validator( diff --git a/tests/core/test_upgrade_transaction_safety.py b/tests/core/test_upgrade_transaction_safety.py index e8aee6fc7..b4e61c98b 100644 --- a/tests/core/test_upgrade_transaction_safety.py +++ b/tests/core/test_upgrade_transaction_safety.py @@ -13,7 +13,7 @@ from orchestrator.cli.models.parameters import AdoUpgradeCommandParameters from orchestrator.cli.utils.generic.wrappers import get_sql_store from orchestrator.cli.utils.resources.handlers import handle_ado_upgrade -from orchestrator.core.legacy.registry import LegacyValidatorRegistry, legacy_validator +from orchestrator.core.legacy.registry import legacy_validator from orchestrator.core.resources import CoreResourceKinds from orchestrator.core.samplestore.config import ( SampleStoreConfiguration, @@ -27,13 +27,10 @@ class TestUpgradeTransactionSafety: """Test transaction safety in upgrade handler - validate-all-before-save pattern""" - def setup_method(self) -> None: - """Clear the registry before each test""" - LegacyValidatorRegistry._validators = {} - @pytest.mark.parametrize("valid_ado_project_context", ["sqlite"], indirect=True) def test_all_resources_validated_before_any_saved( self, + isolated_legacy_validator_registry: None, valid_ado_project_context: ProjectContext, ) -> None: """Test that all resources are validated before any are saved""" diff --git a/tests/core/test_validator_dependencies.py b/tests/core/test_validator_dependencies.py index 4becfeaa4..1f5f7d385 100644 --- a/tests/core/test_validator_dependencies.py +++ b/tests/core/test_validator_dependencies.py @@ -3,8 +3,6 @@ """Tests for validator dependency resolution and ordering""" -from collections.abc import Generator - import pytest from orchestrator.core.legacy.metadata import LegacyValidatorMetadata @@ -12,15 +10,9 @@ from orchestrator.core.resources import CoreResourceKinds -@pytest.fixture(autouse=True) -def clear_registry() -> Generator[None, None, None]: - """Clear the registry before and after each test""" - LegacyValidatorRegistry._validators.clear() - yield - LegacyValidatorRegistry._validators.clear() - - -def test_resolve_dependencies_no_dependencies() -> None: +def test_resolve_dependencies_no_dependencies( + isolated_legacy_validator_registry: None, +) -> None: """Test resolving validators with no dependencies""" def validator_a(data: dict) -> dict: @@ -128,7 +120,9 @@ def validator_c(data: dict) -> dict: assert len(missing) == 0 -def test_resolve_dependencies_diamond() -> None: +def test_resolve_dependencies_diamond( + isolated_legacy_validator_registry: None, +) -> None: """Test resolving validators with diamond dependency pattern""" def validator_a(data: dict) -> dict: @@ -208,7 +202,9 @@ def validator_d(data: dict) -> dict: assert len(missing) == 0 -def test_resolve_dependencies_circular() -> None: +def test_resolve_dependencies_circular( + isolated_legacy_validator_registry: None, +) -> None: """Test that circular dependencies are detected""" def validator_a(data: dict) -> dict: @@ -249,7 +245,9 @@ def validator_b(data: dict) -> dict: LegacyValidatorRegistry.resolve_dependencies(["validator_a", "validator_b"]) -def test_resolve_dependencies_missing() -> None: +def test_resolve_dependencies_missing( + isolated_legacy_validator_registry: None, +) -> None: """Test handling of missing dependencies""" def validator_a(data: dict) -> dict: @@ -277,7 +275,9 @@ def validator_a(data: dict) -> dict: assert "nonexistent_validator" in missing -def test_resolve_dependencies_multiple_roots() -> None: +def test_resolve_dependencies_multiple_roots( + isolated_legacy_validator_registry: None, +) -> None: """Test resolving validators with multiple independent roots""" def validator_a(data: dict) -> dict: From 2e81c54aa85e73acee8267347b679715f45550ea Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 16:18:10 +0000 Subject: [PATCH 44/55] fix(tests): update test Signed-off-by: Alessandro Pomponio --- tests/core/test_legacy_registry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/core/test_legacy_registry.py b/tests/core/test_legacy_registry.py index 4afaeaac6..92fd90a11 100644 --- a/tests/core/test_legacy_registry.py +++ b/tests/core/test_legacy_registry.py @@ -272,10 +272,10 @@ def test_list_all( all_validators = LegacyValidatorRegistry.list_all() assert len(all_validators) == 2 - def test_field_path_matching_with_real_validators(self) -> None: + def test_field_path_matching_with_real_validators( + self, legacy_validators_loaded: None + ) -> None: """Integration test: verify field path matching works with real validators""" - # Import validators to trigger registration - import orchestrator.core.legacy.validators # noqa: F401 # Test 1: discoveryspace properties field should match the properties_field_removal validator validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( From 44e2866f930f1dedff8e9d8b98512754d3bc57fd Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Wed, 25 Mar 2026 16:35:57 +0000 Subject: [PATCH 45/55] fix: use correct paths for deprecated sample store fields Signed-off-by: Alessandro Pomponio --- .../samplestore/entitysource_migrations.py | 78 ++++++++++++------- tests/core/test_legacy_registry.py | 10 ++- 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index 684da3e21..33f02489f 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -15,7 +15,7 @@ @legacy_validator( identifier="samplestore_module_type_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.moduleType"], + deprecated_field_paths=["config.specification.module.moduleType"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleType value from 'entity_source' to 'sample_store'", @@ -23,16 +23,20 @@ def migrate_module_type(data: dict) -> dict: """Convert moduleType from entity_source to sample_store - This validator checks for moduleType field within the config + This validator checks for moduleType field within the config.specification.module and converts it from 'entity_source' to 'sample_store'. Old format: config: - moduleType: "entity_source" + specification: + module: + moduleType: "entity_source" New format: config: - moduleType: "sample_store" + specification: + module: + moduleType: "sample_store" Args: data: The resource data dictionary @@ -44,11 +48,13 @@ def migrate_module_type(data: dict) -> dict: if not isinstance(data, dict): return data - # Check and update config.moduleType - if has_nested_field(data, "config.moduleType"): - module_type = get_nested_value(data, "config.moduleType") + # Check and update config.specification.module.moduleType + if has_nested_field(data, "config.specification.module.moduleType"): + module_type = get_nested_value(data, "config.specification.module.moduleType") if module_type == "entity_source": - set_nested_value(data, "config.moduleType", "sample_store") + set_nested_value( + data, "config.specification.module.moduleType", "sample_store" + ) return data @@ -56,7 +62,7 @@ def migrate_module_type(data: dict) -> dict: @legacy_validator( identifier="samplestore_module_class_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.moduleClass"], + deprecated_field_paths=["config.specification.module.moduleClass"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleClass values from EntitySource to SampleStore naming (CSVEntitySource -> CSVSampleStore, SQLEntitySource -> SQLSampleStore)", @@ -64,16 +70,20 @@ def migrate_module_type(data: dict) -> dict: def migrate_module_class(data: dict) -> dict: """Convert moduleClass from EntitySource to SampleStore naming - This validator checks for moduleClass field within the config + This validator checks for moduleClass field within the config.specification.module and converts it from EntitySource to SampleStore naming. Old format: config: - moduleClass: "CSVEntitySource" or "SQLEntitySource" + specification: + module: + moduleClass: "CSVEntitySource" or "SQLEntitySource" New format: config: - moduleClass: "CSVSampleStore" or "SQLSampleStore" + specification: + module: + moduleClass: "CSVSampleStore" or "SQLSampleStore" Args: data: The resource data dictionary @@ -90,11 +100,15 @@ def migrate_module_class(data: dict) -> dict: "SQLEntitySource": "SQLSampleStore", } - # Check and update config.moduleClass - if has_nested_field(data, "config.moduleClass"): - module_class = get_nested_value(data, "config.moduleClass") + # Check and update config.specification.module.moduleClass + if has_nested_field(data, "config.specification.module.moduleClass"): + module_class = get_nested_value(data, "config.specification.module.moduleClass") if isinstance(module_class, str) and module_class in value_mappings: - set_nested_value(data, "config.moduleClass", value_mappings[module_class]) + set_nested_value( + data, + "config.specification.module.moduleClass", + value_mappings[module_class], + ) return data @@ -102,7 +116,7 @@ def migrate_module_class(data: dict) -> dict: @legacy_validator( identifier="samplestore_module_name_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.moduleName"], + deprecated_field_paths=["config.specification.module.moduleName"], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Updates module paths from entitysource to samplestore (orchestrator.core.entitysource -> orchestrator.core.samplestore)", @@ -110,21 +124,25 @@ def migrate_module_class(data: dict) -> dict: def migrate_module_name(data: dict) -> dict: """Convert moduleName paths from entitysource to samplestore - This validator checks for moduleName field within the config + This validator checks for moduleName field within the config.specification.module and converts paths from entitysource to samplestore using exact matching. Only exact matches are migrated: config: - moduleName: "orchestrator.core.entitysource" - -> "orchestrator.core.samplestore" + specification: + module: + moduleName: "orchestrator.core.entitysource" + -> "orchestrator.core.samplestore" - moduleName: "orchestrator.plugins.entitysources" - -> "orchestrator.plugins.samplestores" + moduleName: "orchestrator.plugins.entitysources" + -> "orchestrator.plugins.samplestores" Submodules or partial matches are NOT migrated: config: - moduleName: "orchestrator.core.entitysource.csv" - -> unchanged (not an exact match) + specification: + module: + moduleName: "orchestrator.core.entitysource.csv" + -> unchanged (not an exact match) Args: data: The resource data dictionary @@ -141,11 +159,15 @@ def migrate_module_name(data: dict) -> dict: "orchestrator.plugins.entitysources": "orchestrator.plugins.samplestores", } - # Check and update config.moduleName - if has_nested_field(data, "config.moduleName"): - module_name = get_nested_value(data, "config.moduleName") + # Check and update config.specification.module.moduleName + if has_nested_field(data, "config.specification.module.moduleName"): + module_name = get_nested_value(data, "config.specification.module.moduleName") if isinstance(module_name, str) and module_name in path_mappings: - set_nested_value(data, "config.moduleName", path_mappings[module_name]) + set_nested_value( + data, + "config.specification.module.moduleName", + path_mappings[module_name], + ) return data diff --git a/tests/core/test_legacy_registry.py b/tests/core/test_legacy_registry.py index 92fd90a11..003dcbe32 100644 --- a/tests/core/test_legacy_registry.py +++ b/tests/core/test_legacy_registry.py @@ -301,9 +301,9 @@ def test_field_path_matching_with_real_validators( validator_ids = {v.identifier for v in validators} assert "randomwalk_mode_to_sampler_config" in validator_ids - # Test 4: samplestore config.moduleType should match the module_type validator + # Test 4: samplestore config.specification.module.moduleType should match the module_type validator validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( - CoreResourceKinds.SAMPLESTORE, {"config.moduleType"} + CoreResourceKinds.SAMPLESTORE, {"config.specification.module.moduleType"} ) assert len(validators) >= 1 validator_ids = {v.identifier for v in validators} @@ -320,7 +320,11 @@ def test_field_path_matching_with_real_validators( # Test 6: Multiple paths should return multiple validators validators = LegacyValidatorRegistry.find_validators_for_deprecated_field_paths( CoreResourceKinds.SAMPLESTORE, - {"config.moduleType", "config.moduleClass", "config.moduleName"}, + { + "config.specification.module.moduleType", + "config.specification.module.moduleClass", + "config.specification.module.moduleName", + }, ) assert len(validators) >= 3 validator_ids = {v.identifier for v in validators} From 7d2e4d3c7d4fb8b1e6672d1c6c27850db7363f5b Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Mar 2026 09:30:43 +0000 Subject: [PATCH 46/55] fix: restore migrate_module_name's original behaviour Signed-off-by: Alessandro Pomponio --- .../samplestore/entitysource_migrations.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index 33f02489f..b37318431 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -125,9 +125,9 @@ def migrate_module_name(data: dict) -> dict: """Convert moduleName paths from entitysource to samplestore This validator checks for moduleName field within the config.specification.module - and converts paths from entitysource to samplestore using exact matching. + and converts paths from entitysource to samplestore using substring replacement. - Only exact matches are migrated: + Migrates any path containing the old module names: config: specification: module: @@ -137,12 +137,8 @@ def migrate_module_name(data: dict) -> dict: moduleName: "orchestrator.plugins.entitysources" -> "orchestrator.plugins.samplestores" - Submodules or partial matches are NOT migrated: - config: - specification: - module: moduleName: "orchestrator.core.entitysource.csv" - -> unchanged (not an exact match) + -> "orchestrator.core.samplestore.csv" Args: data: The resource data dictionary @@ -162,12 +158,15 @@ def migrate_module_name(data: dict) -> dict: # Check and update config.specification.module.moduleName if has_nested_field(data, "config.specification.module.moduleName"): module_name = get_nested_value(data, "config.specification.module.moduleName") - if isinstance(module_name, str) and module_name in path_mappings: - set_nested_value( - data, - "config.specification.module.moduleName", - path_mappings[module_name], - ) + if isinstance(module_name, str): + for old_path, new_path in path_mappings.items(): + if old_path in module_name: + set_nested_value( + data, + "config.specification.module.moduleName", + module_name.replace(old_path, new_path), + ) + break return data From 7072d182eff5e4e60b8ee25838266fef249777ef Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Mar 2026 09:49:16 +0000 Subject: [PATCH 47/55] fix: add deprecated fields to simulate field_validator behaviour Signed-off-by: Alessandro Pomponio --- .../resource/entitysource_to_samplestore.py | 5 + .../samplestore/entitysource_migrations.py | 95 ++++++++++++++++--- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py index 737db2137..fc218e9d5 100644 --- a/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py +++ b/orchestrator/core/legacy/validators/resource/entitysource_to_samplestore.py @@ -15,6 +15,11 @@ deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts resource kind from 'entitysource' to 'samplestore'", + dependencies=[ + "samplestore_module_type_entitysource_to_samplestore", + "samplestore_module_class_entitysource_to_samplestore", + "samplestore_module_name_entitysource_to_samplestore", + ], ) def migrate_entitysource_kind_to_samplestore(data: dict) -> dict: """Migrate old entitysource kind to samplestore diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index b37318431..1ca1b21f2 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -15,7 +15,10 @@ @legacy_validator( identifier="samplestore_module_type_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.specification.module.moduleType"], + deprecated_field_paths=[ + "config.specification.module.moduleType", + "config.copyFrom.0.module.moduleType", + ], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleType value from 'entity_source' to 'sample_store'", @@ -23,20 +26,26 @@ def migrate_module_type(data: dict) -> dict: """Convert moduleType from entity_source to sample_store - This validator checks for moduleType field within the config.specification.module - and converts it from 'entity_source' to 'sample_store'. + This validator checks for moduleType field within config.specification.module + and config.copyFrom[].module, converting them from 'entity_source' to 'sample_store'. Old format: config: specification: module: moduleType: "entity_source" + copyFrom: + - module: + moduleType: "entity_source" New format: config: specification: module: moduleType: "sample_store" + copyFrom: + - module: + moduleType: "sample_store" Args: data: The resource data dictionary @@ -48,7 +57,7 @@ def migrate_module_type(data: dict) -> dict: if not isinstance(data, dict): return data - # Check and update config.specification.module.moduleType + # Update config.specification.module.moduleType if has_nested_field(data, "config.specification.module.moduleType"): module_type = get_nested_value(data, "config.specification.module.moduleType") if module_type == "entity_source": @@ -56,13 +65,28 @@ def migrate_module_type(data: dict) -> dict: data, "config.specification.module.moduleType", "sample_store" ) + # Update config.copyFrom[].module.moduleType + if has_nested_field(data, "config.copyFrom"): + copy_from = get_nested_value(data, "config.copyFrom") + if isinstance(copy_from, list): + for item in copy_from: + if isinstance(item, dict) and has_nested_field( + item, "module.moduleType" + ): + module_type = get_nested_value(item, "module.moduleType") + if module_type == "entity_source": + set_nested_value(item, "module.moduleType", "sample_store") + return data @legacy_validator( identifier="samplestore_module_class_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.specification.module.moduleClass"], + deprecated_field_paths=[ + "config.specification.module.moduleClass", + "config.copyFrom.0.module.moduleClass", + ], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Converts moduleClass values from EntitySource to SampleStore naming (CSVEntitySource -> CSVSampleStore, SQLEntitySource -> SQLSampleStore)", @@ -70,20 +94,26 @@ def migrate_module_type(data: dict) -> dict: def migrate_module_class(data: dict) -> dict: """Convert moduleClass from EntitySource to SampleStore naming - This validator checks for moduleClass field within the config.specification.module - and converts it from EntitySource to SampleStore naming. + This validator checks for moduleClass field within config.specification.module + and config.copyFrom[].module, converting them from EntitySource to SampleStore naming. Old format: config: specification: module: moduleClass: "CSVEntitySource" or "SQLEntitySource" + copyFrom: + - module: + moduleClass: "CSVEntitySource" New format: config: specification: module: moduleClass: "CSVSampleStore" or "SQLSampleStore" + copyFrom: + - module: + moduleClass: "CSVSampleStore" Args: data: The resource data dictionary @@ -100,7 +130,7 @@ def migrate_module_class(data: dict) -> dict: "SQLEntitySource": "SQLSampleStore", } - # Check and update config.specification.module.moduleClass + # Update config.specification.module.moduleClass if has_nested_field(data, "config.specification.module.moduleClass"): module_class = get_nested_value(data, "config.specification.module.moduleClass") if isinstance(module_class, str) and module_class in value_mappings: @@ -110,13 +140,30 @@ def migrate_module_class(data: dict) -> dict: value_mappings[module_class], ) + # Update config.copyFrom[].module.moduleClass + if has_nested_field(data, "config.copyFrom"): + copy_from = get_nested_value(data, "config.copyFrom") + if isinstance(copy_from, list): + for item in copy_from: + if isinstance(item, dict) and has_nested_field( + item, "module.moduleClass" + ): + module_class = get_nested_value(item, "module.moduleClass") + if isinstance(module_class, str) and module_class in value_mappings: + set_nested_value( + item, "module.moduleClass", value_mappings[module_class] + ) + return data @legacy_validator( identifier="samplestore_module_name_entitysource_to_samplestore", resource_type=CoreResourceKinds.SAMPLESTORE, - deprecated_field_paths=["config.specification.module.moduleName"], + deprecated_field_paths=[ + "config.specification.module.moduleName", + "config.copyFrom.0.module.moduleName", + ], deprecated_from_version="0.9.6", removed_from_version="1.0.0", description="Updates module paths from entitysource to samplestore (orchestrator.core.entitysource -> orchestrator.core.samplestore)", @@ -124,8 +171,9 @@ def migrate_module_class(data: dict) -> dict: def migrate_module_name(data: dict) -> dict: """Convert moduleName paths from entitysource to samplestore - This validator checks for moduleName field within the config.specification.module - and converts paths from entitysource to samplestore using substring replacement. + This validator checks for moduleName field within config.specification.module + and config.copyFrom[].module, converting paths from entitysource to samplestore + using substring replacement. Migrates any path containing the old module names: config: @@ -139,6 +187,10 @@ def migrate_module_name(data: dict) -> dict: moduleName: "orchestrator.core.entitysource.csv" -> "orchestrator.core.samplestore.csv" + copyFrom: + - module: + moduleName: "orchestrator.core.entitysource.sql" + -> "orchestrator.core.samplestore.sql" Args: data: The resource data dictionary @@ -155,7 +207,7 @@ def migrate_module_name(data: dict) -> dict: "orchestrator.plugins.entitysources": "orchestrator.plugins.samplestores", } - # Check and update config.specification.module.moduleName + # Update config.specification.module.moduleName if has_nested_field(data, "config.specification.module.moduleName"): module_name = get_nested_value(data, "config.specification.module.moduleName") if isinstance(module_name, str): @@ -168,6 +220,25 @@ def migrate_module_name(data: dict) -> dict: ) break + # Update config.copyFrom[].module.moduleName + if has_nested_field(data, "config.copyFrom"): + copy_from = get_nested_value(data, "config.copyFrom") + if isinstance(copy_from, list): + for item in copy_from: + if isinstance(item, dict) and has_nested_field( + item, "module.moduleName" + ): + module_name = get_nested_value(item, "module.moduleName") + if isinstance(module_name, str): + for old_path, new_path in path_mappings.items(): + if old_path in module_name: + set_nested_value( + item, + "module.moduleName", + module_name.replace(old_path, new_path), + ) + break + return data From 29de01ee3b336bc1817050016f63f8d03b6d9b61 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Mar 2026 10:31:37 +0000 Subject: [PATCH 48/55] feat(legacy): add support for gt4sd migration Signed-off-by: Alessandro Pomponio --- .../legacy/validators/samplestore/__init__.py | 7 +- .../gt4sd_transformer_migration.py | 135 ++++++++ .../test_gt4sd_transformer_migration.py | 289 ++++++++++++++++++ 3 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 orchestrator/core/legacy/validators/samplestore/gt4sd_transformer_migration.py create mode 100644 tests/core/legacy/validators/samplestore/test_gt4sd_transformer_migration.py diff --git a/orchestrator/core/legacy/validators/samplestore/__init__.py b/orchestrator/core/legacy/validators/samplestore/__init__.py index 4dec63489..9fa059605 100644 --- a/orchestrator/core/legacy/validators/samplestore/__init__.py +++ b/orchestrator/core/legacy/validators/samplestore/__init__.py @@ -5,9 +5,14 @@ from orchestrator.core.legacy.validators.samplestore import ( entitysource_migrations, + gt4sd_transformer_migration, v1_to_v2_csv_migration, ) -__all__ = ["entitysource_migrations", "v1_to_v2_csv_migration"] +__all__ = [ + "entitysource_migrations", + "gt4sd_transformer_migration", + "v1_to_v2_csv_migration", +] # Made with Bob diff --git a/orchestrator/core/legacy/validators/samplestore/gt4sd_transformer_migration.py b/orchestrator/core/legacy/validators/samplestore/gt4sd_transformer_migration.py new file mode 100644 index 000000000..ccdab462f --- /dev/null +++ b/orchestrator/core/legacy/validators/samplestore/gt4sd_transformer_migration.py @@ -0,0 +1,135 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Legacy validator for migrating GT4SDTransformer to CSVSampleStore""" + +from orchestrator.core.legacy.registry import legacy_validator +from orchestrator.core.legacy.utils import ( + get_nested_value, + has_nested_field, + set_nested_value, +) +from orchestrator.core.resources import CoreResourceKinds + + +@legacy_validator( + identifier="samplestore_gt4sd_transformer_to_csv", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_field_paths=[ + "config.copyFrom.0.module.moduleClass", + "config.copyFrom.0.module.moduleName", + ], + deprecated_from_version="1.3.5", + removed_from_version="1.6.0", + description="Converts GT4SDTransformer plugin to CSVSampleStore with explicit parameters", +) +def migrate_gt4sd_transformer_to_csv(data: dict) -> dict: + """Migrate GT4SDTransformer plugin usage to CSVSampleStore with explicit parameters + + The GT4SDTransformer class was a thin wrapper around CSVSampleStore that + automatically filled in parameters. This validator converts configurations + using GT4SDTransformer to use CSVSampleStore directly with explicit parameters. + + Old format: + config: + copyFrom: + - module: + moduleClass: GT4SDTransformer + moduleName: orchestrator.plugins.samplestores.gt4sd + parameters: + generatorIdentifier: 'gt4sd-pfas-transformer-model-one' + + New format: + config: + copyFrom: + - module: + moduleClass: CSVSampleStore + moduleName: orchestrator.core.samplestore.csv + parameters: + generatorIdentifier: 'gt4sd-pfas-transformer-model-one' + identifierColumn: 'smiles' + experiments: + - experimentIdentifier: 'transformer-toxicity-inference-experiment' + observedPropertyMap: + logws: GenLogws + logd: GenLogd + loghl: GenLoghl + pka: GenPka + "biodegradation halflife": GenBiodeg + bcf: GenBcf + ld50: GenLd50 + scscore: GenScscore + constitutivePropertyMap: [smiles] + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + # Property map from the old GT4SDTransformer class + property_map = { + "logws": "GenLogws", + "logd": "GenLogd", + "loghl": "GenLoghl", + "pka": "GenPka", + "biodegradation halflife": "GenBiodeg", + "bcf": "GenBcf", + "ld50": "GenLd50", + "scscore": "GenScscore", + } + + # Check config.copyFrom array for GT4SDTransformer usage + if has_nested_field(data, "config.copyFrom"): + copy_from = get_nested_value(data, "config.copyFrom") + if isinstance(copy_from, list): + for item in copy_from: + if not isinstance(item, dict): + continue + + # Check if this is a GT4SDTransformer module + module_class = get_nested_value(item, "module.moduleClass") + module_name = get_nested_value(item, "module.moduleName") + + if ( + module_class == "GT4SDTransformer" + and module_name == "orchestrator.plugins.samplestores.gt4sd" + ): + # Update module class and name + set_nested_value(item, "module.moduleClass", "CSVSampleStore") + set_nested_value( + item, "module.moduleName", "orchestrator.core.samplestore.csv" + ) + + # Add explicit parameters that GT4SDTransformer provided automatically + if not has_nested_field(item, "parameters"): + set_nested_value(item, "parameters", {}) + + parameters = get_nested_value(item, "parameters") + + # Add identifierColumn if not present + if ( + isinstance(parameters, dict) + and "identifierColumn" not in parameters + ): + set_nested_value(item, "parameters.identifierColumn", "smiles") + + # Add experiments configuration if not present + if isinstance(parameters, dict) and "experiments" not in parameters: + experiment_config = { + "experimentIdentifier": "transformer-toxicity-inference-experiment", + "observedPropertyMap": property_map, + "constitutivePropertyMap": ["smiles"], + } + set_nested_value( + item, "parameters.experiments", [experiment_config] + ) + + return data + + +# Made with Bob diff --git a/tests/core/legacy/validators/samplestore/test_gt4sd_transformer_migration.py b/tests/core/legacy/validators/samplestore/test_gt4sd_transformer_migration.py new file mode 100644 index 000000000..a075db4d3 --- /dev/null +++ b/tests/core/legacy/validators/samplestore/test_gt4sd_transformer_migration.py @@ -0,0 +1,289 @@ +# Copyright IBM Corporation 2025, 2026 +# SPDX-License-Identifier: MIT + +"""Tests for GT4SDTransformer to CSVSampleStore migration validator""" + +from orchestrator.core.legacy.registry import LegacyValidatorRegistry + + +class TestGT4SDTransformerMigration: + """Test migrate_gt4sd_transformer_to_csv validator""" + + def test_migrates_gt4sd_transformer_to_csv_sample_store( + self, legacy_validators_loaded: None + ) -> None: + """Test that GT4SDTransformer is migrated to CSVSampleStore with explicit parameters""" + validator = LegacyValidatorRegistry.get_validator( + "samplestore_gt4sd_transformer_to_csv" + ) + assert validator is not None + + data = { + "config": { + "copyFrom": [ + { + "module": { + "moduleClass": "GT4SDTransformer", + "moduleName": "orchestrator.plugins.samplestores.gt4sd", + }, + "storageLocation": { + "path": "data/GM_Comparison/Transfromer/Sample_0/test_generations.csv" + }, + "parameters": { + "generatorIdentifier": "gt4sd-pfas-transformer-model-one" + }, + } + ] + } + } + + result = validator.validator_function(data) + + # Check module class and name were updated + copy_from = result["config"]["copyFrom"][0] + assert copy_from["module"]["moduleClass"] == "CSVSampleStore" + assert copy_from["module"]["moduleName"] == "orchestrator.core.samplestore.csv" + + # Check identifierColumn was added + assert copy_from["parameters"]["identifierColumn"] == "smiles" + + # Check experiments configuration was added + assert "experiments" in copy_from["parameters"] + experiments = copy_from["parameters"]["experiments"] + assert len(experiments) == 1 + assert ( + experiments[0]["experimentIdentifier"] + == "transformer-toxicity-inference-experiment" + ) + assert "observedPropertyMap" in experiments[0] + assert "constitutivePropertyMap" in experiments[0] + assert experiments[0]["constitutivePropertyMap"] == ["smiles"] + + # Check property map was correctly added + property_map = experiments[0]["observedPropertyMap"] + assert property_map["logws"] == "GenLogws" + assert property_map["logd"] == "GenLogd" + assert property_map["loghl"] == "GenLoghl" + assert property_map["pka"] == "GenPka" + assert property_map["biodegradation halflife"] == "GenBiodeg" + assert property_map["bcf"] == "GenBcf" + assert property_map["ld50"] == "GenLd50" + assert property_map["scscore"] == "GenScscore" + + # Check original generatorIdentifier was preserved + assert ( + copy_from["parameters"]["generatorIdentifier"] + == "gt4sd-pfas-transformer-model-one" + ) + + def test_preserves_existing_identifier_column( + self, legacy_validators_loaded: None + ) -> None: + """Test that existing identifierColumn is not overwritten""" + validator = LegacyValidatorRegistry.get_validator( + "samplestore_gt4sd_transformer_to_csv" + ) + assert validator is not None + + data = { + "config": { + "copyFrom": [ + { + "module": { + "moduleClass": "GT4SDTransformer", + "moduleName": "orchestrator.plugins.samplestores.gt4sd", + }, + "parameters": { + "generatorIdentifier": "gt4sd-pfas-transformer-model-one", + "identifierColumn": "custom_id", + }, + } + ] + } + } + + result = validator.validator_function(data) + + # Check that custom identifierColumn was preserved + assert ( + result["config"]["copyFrom"][0]["parameters"]["identifierColumn"] + == "custom_id" + ) + + def test_preserves_existing_experiments( + self, legacy_validators_loaded: None + ) -> None: + """Test that existing experiments configuration is not overwritten""" + validator = LegacyValidatorRegistry.get_validator( + "samplestore_gt4sd_transformer_to_csv" + ) + assert validator is not None + + custom_experiments = [ + { + "experimentIdentifier": "custom-experiment", + "observedPropertyMap": {"prop1": "Prop1"}, + "constitutivePropertyMap": ["id"], + } + ] + + data = { + "config": { + "copyFrom": [ + { + "module": { + "moduleClass": "GT4SDTransformer", + "moduleName": "orchestrator.plugins.samplestores.gt4sd", + }, + "parameters": { + "generatorIdentifier": "gt4sd-pfas-transformer-model-one", + "experiments": custom_experiments, + }, + } + ] + } + } + + result = validator.validator_function(data) + + # Check that custom experiments were preserved + assert ( + result["config"]["copyFrom"][0]["parameters"]["experiments"] + == custom_experiments + ) + + def test_does_not_modify_other_module_classes( + self, legacy_validators_loaded: None + ) -> None: + """Test that other module classes are not modified""" + validator = LegacyValidatorRegistry.get_validator( + "samplestore_gt4sd_transformer_to_csv" + ) + assert validator is not None + + data = { + "config": { + "copyFrom": [ + { + "module": { + "moduleClass": "CSVSampleStore", + "moduleName": "orchestrator.core.samplestore.csv", + }, + "parameters": {"identifierColumn": "id"}, + } + ] + } + } + + result = validator.validator_function(data) + + # Check that nothing was changed + copy_from = result["config"]["copyFrom"][0] + assert copy_from["module"]["moduleClass"] == "CSVSampleStore" + assert copy_from["module"]["moduleName"] == "orchestrator.core.samplestore.csv" + assert copy_from["parameters"] == {"identifierColumn": "id"} + + def test_handles_multiple_copy_from_entries( + self, legacy_validators_loaded: None + ) -> None: + """Test that validator handles multiple copyFrom entries correctly""" + validator = LegacyValidatorRegistry.get_validator( + "samplestore_gt4sd_transformer_to_csv" + ) + assert validator is not None + + data = { + "config": { + "copyFrom": [ + { + "module": { + "moduleClass": "GT4SDTransformer", + "moduleName": "orchestrator.plugins.samplestores.gt4sd", + }, + "parameters": {"generatorIdentifier": "model-one"}, + }, + { + "module": { + "moduleClass": "CSVSampleStore", + "moduleName": "orchestrator.core.samplestore.csv", + }, + "parameters": {"identifierColumn": "id"}, + }, + ] + } + } + + result = validator.validator_function(data) + + # Check first entry was migrated + first_entry = result["config"]["copyFrom"][0] + assert first_entry["module"]["moduleClass"] == "CSVSampleStore" + assert ( + first_entry["module"]["moduleName"] == "orchestrator.core.samplestore.csv" + ) + assert "identifierColumn" in first_entry["parameters"] + assert "experiments" in first_entry["parameters"] + + # Check second entry was not modified + second_entry = result["config"]["copyFrom"][1] + assert second_entry["module"]["moduleClass"] == "CSVSampleStore" + assert second_entry["parameters"] == {"identifierColumn": "id"} + + def test_handles_missing_copy_from(self, legacy_validators_loaded: None) -> None: + """Test that validator handles missing copyFrom field gracefully""" + validator = LegacyValidatorRegistry.get_validator( + "samplestore_gt4sd_transformer_to_csv" + ) + assert validator is not None + + data = {"config": {"specification": {"module": {}}}} + + result = validator.validator_function(data) + + # Check that data was not modified + assert result == data + + def test_handles_empty_copy_from(self, legacy_validators_loaded: None) -> None: + """Test that validator handles empty copyFrom array gracefully""" + validator = LegacyValidatorRegistry.get_validator( + "samplestore_gt4sd_transformer_to_csv" + ) + assert validator is not None + + data = {"config": {"copyFrom": []}} + + result = validator.validator_function(data) + + # Check that data was not modified + assert result == data + + def test_handles_missing_parameters(self, legacy_validators_loaded: None) -> None: + """Test that validator adds parameters if missing""" + validator = LegacyValidatorRegistry.get_validator( + "samplestore_gt4sd_transformer_to_csv" + ) + assert validator is not None + + data = { + "config": { + "copyFrom": [ + { + "module": { + "moduleClass": "GT4SDTransformer", + "moduleName": "orchestrator.plugins.samplestores.gt4sd", + }, + } + ] + } + } + + result = validator.validator_function(data) + + # Check that parameters were added + copy_from = result["config"]["copyFrom"][0] + assert "parameters" in copy_from + assert "identifierColumn" in copy_from["parameters"] + assert "experiments" in copy_from["parameters"] + + +# Made with Bob From f5d43b6c7e5feeee8b75fb768b56f8589372e873 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Mar 2026 10:32:20 +0000 Subject: [PATCH 49/55] feat(legacy): add support for removing storageLocation Signed-off-by: Alessandro Pomponio --- .../samplestore/entitysource_migrations.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py index 1ca1b21f2..10cca3c18 100644 --- a/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py +++ b/orchestrator/core/legacy/validators/samplestore/entitysource_migrations.py @@ -242,4 +242,49 @@ def migrate_module_name(data: dict) -> dict: return data +@legacy_validator( + identifier="samplestore_remove_specification_storage_location", + resource_type=CoreResourceKinds.SAMPLESTORE, + deprecated_field_paths=[ + "config.specification.storageLocation", + "config.copyFrom.0.storageLocation", + ], + deprecated_from_version="0.9.6", + removed_from_version="1.0.0", + description="Removes deprecated config.specification.storageLocation field", +) +def remove_specification_storage_location(data: dict) -> dict: + """Remove deprecated config.specification.storageLocation field + + The storageLocation field was moved from config.specification to the top level + of the specification. This validator removes the old nested location. + + Old format: + config: + specification: + storageLocation: {...} + + New format: + config: + specification: + # storageLocation removed from here + + Args: + data: The resource data dictionary + + Returns: + The migrated resource data dictionary + """ + + if not isinstance(data, dict): + return data + + # Remove config.specification.storageLocation if it exists + from orchestrator.core.legacy.utils import remove_nested_field + + remove_nested_field(data, "config.specification.storageLocation") + + return data + + # Made with Bob From 9bf2219e1fe4865ebcfb32ccaa190d8e9c85f640 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Mar 2026 10:32:53 +0000 Subject: [PATCH 50/55] refactor(cli): replace error print with info Signed-off-by: Alessandro Pomponio --- orchestrator/cli/utils/resources/handlers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/orchestrator/cli/utils/resources/handlers.py b/orchestrator/cli/utils/resources/handlers.py index b07ead126..473d49916 100644 --- a/orchestrator/cli/utils/resources/handlers.py +++ b/orchestrator/cli/utils/resources/handlers.py @@ -22,6 +22,7 @@ ADO_SPINNER_QUERYING_DB, ADO_SPINNER_SAVING_TO_DB, ERROR, + INFO, SUCCESS, console_print, cyan, @@ -397,7 +398,7 @@ def handle_ado_upgrade( stderr=True, ) console_print( - f"{ERROR}No resources were modified (all-or-nothing transaction safety)", + f"{INFO}No resources were modified (all-or-nothing transaction safety)", stderr=True, ) raise typer.Exit(1) from e From 5a54b5e8e1bea8959b399986fefd7bea2135d689 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Mar 2026 11:12:52 +0000 Subject: [PATCH 51/55] test: force mysql on tests due to CI issues Signed-off-by: Alessandro Pomponio --- tests/core/test_legacy_validators.py | 11 +++++++++++ tests/core/test_upgrade_transaction_safety.py | 8 ++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/core/test_legacy_validators.py b/tests/core/test_legacy_validators.py index c7ed625a9..6f031b00f 100644 --- a/tests/core/test_legacy_validators.py +++ b/tests/core/test_legacy_validators.py @@ -3,15 +3,19 @@ """Integration tests for legacy validators with pydantic models and upgrade process""" +import sqlite3 from collections.abc import Callable from pathlib import Path import pydantic +import pytest from orchestrator.core.legacy.registry import LegacyValidatorRegistry, legacy_validator from orchestrator.core.resources import CoreResourceKinds from orchestrator.metastore.project import ProjectContext +sqlite3_version = sqlite3.sqlite_version_info + class TestLegacyValidatorWithPydantic: """Test legacy validators working with pydantic models""" @@ -180,6 +184,7 @@ def step2(data: dict) -> dict: class TestUpgradeHandlerIntegration: """Integration tests for ado upgrade with legacy validators via CLI""" + @pytest.mark.parametrize("valid_ado_project_context", ["mysql"], indirect=True) def test_upgrade_applies_legacy_validator_via_cli( self, legacy_validators_loaded: None, @@ -347,6 +352,12 @@ def test_upgrade_rejects_unknown_validator( "unknown" in result.output.lower() or "not found" in result.output.lower() ) + # AP: the -> and ->> syntax in SQLite is only supported from version 3.38.0 + # ref: https://sqlite.org/json1.html#jptr + @pytest.mark.skipif( + sqlite3_version < (3, 38, 0), + reason="SQLite version 3.38.0 or higher is required", + ) def test_upgrade_auto_resolves_validator_dependencies( self, legacy_validators_loaded: None, diff --git a/tests/core/test_upgrade_transaction_safety.py b/tests/core/test_upgrade_transaction_safety.py index b4e61c98b..1f24a005f 100644 --- a/tests/core/test_upgrade_transaction_safety.py +++ b/tests/core/test_upgrade_transaction_safety.py @@ -4,6 +4,7 @@ """Integration tests for Phase 1 transaction safety in upgrade handler""" import json +import sqlite3 import pytest import sqlalchemy @@ -23,11 +24,13 @@ from orchestrator.core.samplestore.resource import SampleStoreResource from orchestrator.metastore.project import ProjectContext +sqlite3_version = sqlite3.sqlite_version_info + class TestUpgradeTransactionSafety: """Test transaction safety in upgrade handler - validate-all-before-save pattern""" - @pytest.mark.parametrize("valid_ado_project_context", ["sqlite"], indirect=True) + @pytest.mark.parametrize("valid_ado_project_context", ["mysql"], indirect=True) def test_all_resources_validated_before_any_saved( self, isolated_legacy_validator_registry: None, @@ -135,7 +138,7 @@ def test_validator(data: dict) -> dict: assert "old_field" not in upgraded_res1["config"]["metadata"] assert "old_field" not in upgraded_res2["config"]["metadata"] - @pytest.mark.parametrize("valid_ado_project_context", ["sqlite"], indirect=True) + @pytest.mark.parametrize("valid_ado_project_context", ["mysql"], indirect=True) def test_validation_failure_prevents_all_saves( self, valid_ado_project_context: ProjectContext, @@ -254,6 +257,7 @@ def test_validator(data: dict) -> dict: assert "new_field" not in current_res1["config"]["metadata"] assert "new_field" not in current_res2["config"]["metadata"] + @pytest.mark.parametrize("valid_ado_project_context", ["mysql"], indirect=True) def test_empty_resource_list_handled_gracefully( self, valid_ado_project_context: ProjectContext, From b05ae29158170dddc7e8609bbae9593418812a4c Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Mar 2026 12:38:33 +0000 Subject: [PATCH 52/55] fix: use enum value Signed-off-by: Alessandro Pomponio --- orchestrator/metastore/sql/statements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchestrator/metastore/sql/statements.py b/orchestrator/metastore/sql/statements.py index 19a5e6689..ee6cfc560 100644 --- a/orchestrator/metastore/sql/statements.py +++ b/orchestrator/metastore/sql/statements.py @@ -352,7 +352,7 @@ def resource_upsert( r"ON DUPLICATE KEY UPDATE data = values(data)" ).bindparams( identifier=resource.identifier, - kind=resource.kind, + kind=resource.kind.value, version=resource.version, data=json_representation, ) From d53db7b45f28e2d429887776a3b3bc77720fc096 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Mar 2026 13:14:23 +0000 Subject: [PATCH 53/55] fix: reload validators module to ensure we actually have everything loaded Signed-off-by: Alessandro Pomponio --- tests/conftest.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f2e91ee2e..a94dcbfec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ # Copyright IBM Corporation 2025, 2026 # SPDX-License-Identifier: MIT +import importlib import uuid from collections.abc import Callable, Generator @@ -92,20 +93,27 @@ def legacy_validators_loaded() -> Generator[None, None, None]: This fixture: 1. Imports the validators module to trigger registration - 2. Saves a copy of the registered validators - 3. Provides isolation so test modifications don't affect other tests - 4. Restores the validators after the test + 2. Uses importlib.reload() to force re-execution even if cached + 3. Saves a copy of the registered validators + 4. Provides isolation so test modifications don't affect other tests + 5. Restores the validators after the test Use this when your test needs the actual validators to be registered (e.g., for integration tests that use real validators). + IMPORTANT: We use importlib.reload() to ensure the module is re-executed + even if it was previously imported and cached by Python or pytest-xdist workers. + Usage: def test_with_real_validators(legacy_validators_loaded): # All validators are registered and available # Test can use them without affecting other tests """ - # Import to trigger registration (safe - only runs once per process) - import orchestrator.core.legacy.validators # noqa: F401 + # Import to trigger registration + import orchestrator.core.legacy.validators + + # Force reload to ensure decorators execute even if module was cached + importlib.reload(orchestrator.core.legacy.validators) # Save the current state (includes all registered validators) original_validators = LegacyValidatorRegistry._validators.copy() From 2a669e586862f313428da016c34cb538d65d00be Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Thu, 26 Mar 2026 13:52:31 +0000 Subject: [PATCH 54/55] fix: use session-scoped fixture to ensure state is consistent Signed-off-by: Alessandro Pomponio --- tests/conftest.py | 55 +++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a94dcbfec..fea939378 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ # Copyright IBM Corporation 2025, 2026 # SPDX-License-Identifier: MIT -import importlib import uuid from collections.abc import Callable, Generator @@ -56,6 +55,24 @@ def initialize_ray() -> Generator[None, None, None]: ray.shutdown() +@pytest.fixture(scope="session", autouse=True) +def session_legacy_validators() -> dict: + """Load legacy validators once per session and return a copy. + + This session-scoped fixture ensures validators are loaded once at the start + of the test session and the registered validators are saved. This copy can + then be used by test-scoped fixtures to reset the registry state. + + Returns: + A dictionary copy of all registered validators + """ + # Import to trigger registration - this happens once per test session + import orchestrator.core.legacy.validators # noqa: F401 + + # Return a copy of the registered validators + return LegacyValidatorRegistry._validators.copy() + + @pytest.fixture def isolated_legacy_validator_registry() -> Generator[None, None, None]: """Isolate the LegacyValidatorRegistry for each test. @@ -88,38 +105,34 @@ def test_something(isolated_legacy_validator_registry): @pytest.fixture -def legacy_validators_loaded() -> Generator[None, None, None]: +def legacy_validators_loaded( + session_legacy_validators: dict, +) -> Generator[None, None, None]: """Ensure legacy validators are loaded and isolated for the test. This fixture: - 1. Imports the validators module to trigger registration - 2. Uses importlib.reload() to force re-execution even if cached - 3. Saves a copy of the registered validators - 4. Provides isolation so test modifications don't affect other tests - 5. Restores the validators after the test + 1. Resets the registry to the session state (all validators loaded) + 2. Allows the test to run (potentially modifying the registry) + 3. Restores the registry to the session state after the test - Use this when your test needs the actual validators to be registered - (e.g., for integration tests that use real validators). + This ensures: + - All validators are available to the test + - Test modifications don't affect other tests + - Consistent behavior across pytest-xdist workers - IMPORTANT: We use importlib.reload() to ensure the module is re-executed - even if it was previously imported and cached by Python or pytest-xdist workers. + The session_legacy_validators fixture loads validators once per test session, + and this fixture resets to that known-good state before and after each test. Usage: def test_with_real_validators(legacy_validators_loaded): # All validators are registered and available # Test can use them without affecting other tests """ - # Import to trigger registration - import orchestrator.core.legacy.validators - - # Force reload to ensure decorators execute even if module was cached - importlib.reload(orchestrator.core.legacy.validators) - - # Save the current state (includes all registered validators) - original_validators = LegacyValidatorRegistry._validators.copy() + # Reset registry to session state before test + LegacyValidatorRegistry._validators = session_legacy_validators.copy() try: yield finally: - # Restore to ensure other tests see the same state - LegacyValidatorRegistry._validators = original_validators + # Restore registry to session state after test + LegacyValidatorRegistry._validators = session_legacy_validators.copy() From 17e8084dcf144d8418147935d83526635bd508f2 Mon Sep 17 00:00:00 2001 From: Alessandro Pomponio Date: Fri, 27 Mar 2026 09:33:16 +0000 Subject: [PATCH 55/55] docs(website): update ado upgrade documentation Signed-off-by: Alessandro Pomponio --- website/docs/getting-started/ado.md | 128 ++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 7 deletions(-) diff --git a/website/docs/getting-started/ado.md b/website/docs/getting-started/ado.md index 3d82db393..8b1af6575 100644 --- a/website/docs/getting-started/ado.md +++ b/website/docs/getting-started/ado.md @@ -1,7 +1,8 @@ - + + !!! note This page provides documentation for the `ado` CLI tool, which needs to be @@ -97,6 +98,8 @@ Where: - `RESOURCE_TYPE` is one of the supported resource types for `ado create`, currently: + + - _actuator_ - _actuatorconfiguration_ (_ac_) - _context_ (_ctx_) @@ -104,6 +107,8 @@ Where: - _samplestore_ (_store_) - _discoveryspace_ (_space_) + + - `--file` or `-f` is a path to the resource configuration file in YAML format. It is mandatory in all scenarios, except when running `ado create samplestore --new-sample-store`. @@ -217,6 +222,8 @@ Where: - `RESOURCE_TYPE` is the type of resource you want to delete. Currently, the only supported types are: + + - _actuatorconfiguration_ (_ac_) - _context_ (_ctx_) - _datacontainer_ (_dcr_) @@ -224,10 +231,18 @@ Where: - _samplestore_ (_store_) - _discoveryspace_ (_space_) + + - `RESOURCE_ID` is the unique identifier of the resource to delete. - `--force` enables forced deletion of resources in the following cases: + + + - When attempting to delete operations while other operations are executing. - When attempting to delete sample stores that still contain data. + + + - When deleting a local context, users can specify the flags `--delete-local-db` or `--no-delete-local-db` to explicitly delete or preserve a local DB when deleting its related context. If neither of these flags are specified, the @@ -270,10 +285,14 @@ Where: - `RESOURCE_TYPE` is the type of resource you want to describe. Currently, the supported resource types are: + + - _experiment_ - _datacontainer_ (_dcr_) - _discoveryspace_ (_space_) + + - `RESOURCE_ID` is the unique identifier of the resource to describe. - The `--file` (or `-f`) flag is **currently only available for spaces** and allows getting a description of the space, given a space configuration file. @@ -306,20 +325,28 @@ Where: - `RESOURCE_TYPE` is the type of resource you want to edit. Supported types are: + + - _actuatorconfiguration_ (_ac_) - _datacontainer_ (_dcr_) - _operation_ (_op_) - _samplestore_ (_store_) - _discoveryspace_ (_space_) + + - `RESOURCE_ID` is the unique identifier of the resource to edit. - `--editor` is the name of the editor you want to use for editing metadata. It must be one of the supported ones, which currently are: + + - `vim` (_default_) - `vi` - `nano` + + Alternatively, you can also set the value for this flag by using the environment variable `ADO_EDITOR`. @@ -376,6 +403,8 @@ Where: - `RESOURCE_TYPE` is the type of resource you want to get. Currently, the only supported types are: + + - _actuatorconfiguration_ (_ac_) - _actuator_ - _context_ (_ctx_) @@ -386,9 +415,13 @@ Where: - _samplestore_ (_store_) - _discoveryspace_ (_space_) + + - `RESOURCE_ID` is the optional unique identifier of the resource to get. - `--output` or `-o` determine the type of output that will be displayed: + + - The `default` format shows the _identifier_, the _name_, and the _age_ of the matching resources. - The `yaml` format displays the full YAML document of the matching resources. @@ -397,6 +430,8 @@ Where: - The `raw` format displays the raw resource as stored in the database, performing no validation. + + - `--exclude-default` (set by default) allows excluding fields that use default values from the output. Alternatively, the `--no-exclude-default` flag can be used to show them. @@ -422,9 +457,9 @@ Where: - When using the `--details` flag with the `default` output format, additional columns with the _description_ and the _labels_ of the matching resources are printed. -- The `--show-deprecated` flag is available **only for - `ado get experiments`** and allows displaying experiments that have - been deprecated. They are otherwise hidden by default. +- The `--show-deprecated` flag is available **only for `ado get experiments`** + and allows displaying experiments that have been deprecated. They are + otherwise hidden by default. #### Searching and Filtering @@ -464,7 +499,9 @@ ado get spaces --details ``` + ##### Getting all Discovery Spaces that include granite-7b-base in the property domain + !!! info @@ -513,7 +550,9 @@ ado get space space-df8077-7535f9 -o yaml \ ``` + ##### Getting an actuator configuration and hiding the status for the "created" event + ```shell @@ -570,9 +609,13 @@ Where: - `RESOURCE_TYPE` is one of the supported resource types: + + - _operation_ (_op_) - _discoveryspace_ (_space_) + + - `RESOURCE_ID` is the unique identifier of the resource you want to see details for. - `--use-latest` will use the identifier of the latest (i.e. most recent) @@ -613,9 +656,13 @@ Where: - `RESOURCE_TYPE` is one of the supported resource types: + + - _operation_ (_op_) - _discoveryspace_ (_space_) + + - `RESOURCE_ID` is the unique identifier of the resource you want to see entities for. - `--use-latest` will use the identifier of the latest (i.e. most recent) @@ -627,22 +674,32 @@ Where: - `--property-format` defines the naming format used for measured properties in the output, one of: + + - `observed`: properties are named `$experimentid.$property_id`. There will be one row per entity. - `target`: properties are named `$property_id`. There will be one row per (entity, experiment) pair. + + - `--output-format` is the format in which to display the entity data. One of: + + - `console` (print to stdout) - `csv` (output as CSV file) - `json` (output as JSON file) + + - `--property` (can be specified multiple times) is used to filter what measured properties need to be output. - `--include` (**exclusive to spaces**) determines what type of entities to include. One of: + + - `sampled`: Entities that have been measured by explore operations on the `discoveryspace` - `unsampled`: Entities that have not been measured by an explore operation @@ -652,8 +709,13 @@ Where: - `missing`: Entities in the `discoveryspace` that are not in the `samplestore` the `discoveryspace` uses + + - `--aggregate` allows applying an aggregation to the result values in case multiple are present. One of: + + + - `mean` - `median` - `variance` @@ -661,6 +723,8 @@ Where: - `min` - `max` + + ##### Examples ###### Show matching entities in a Space with target format and output them as CSV @@ -671,8 +735,9 @@ Where: --output-format csv ``` - + ###### Show a subset of the properties of entities that are part of an operation and output them as JSON + ```shell ado show entities operation randomwalk-0.5.0-123abc --output-format json \ @@ -775,10 +840,14 @@ ado show related RESOURCE_TYPE [RESOURCE_ID] [--use-latest] - `RESOURCE_TYPE` is one of the supported resource types: + + - _operation_ (_op_) - _samplestore_ (_store_) - _discoveryspace_ (_space_) + + - `RESOURCE_ID` is the unique identifier of the resource you want to see related resources for. - `--use-latest` will use the identifier of the latest (i.e. most recent) @@ -832,10 +901,15 @@ Where: properties. Cannot be used when the output format is `md`. - `--format | -o` allows choosing the output format in which the information should be displayed. Can be one of either: + + + - `md` - for Markdown text. - `table` (**default**) - for Markdown tables. - `csv` - for a comma separated file. + + ##### Examples ###### Get the summary of a space as a Markdown table @@ -845,7 +919,9 @@ ado show summary space space-abc123-456def ``` + ###### Get the summary of a space as a Markdown table and include the constitutive property MY_PROPERTY + ```shell @@ -915,12 +991,16 @@ Where: - `RESOURCE_TYPE` is one of the supported resource types: + + - _actuator_ - _actuatorconfiguration_ (_ac_) - _context_ (_ctx_) - _operation_ (_op_) - _discoveryspace_ (_space_) + + - `--output` or `-o` can be used to point to a location where to save the template. By default, the template will be saved in the current directory with an autogenerated name. @@ -932,6 +1012,8 @@ Where: - `--operator-type` (**exclusive for operations**) is the type of operator to generate a template for. Must be one of the supported operator types: + + - `characterize` - `search` - `compare` @@ -940,6 +1022,8 @@ Where: - `fuse` - `learn` + + - `--actuator-configuration` (**exclusive for actuatorconfigurations**) is the identifier of the actuator to output. If unset, a generic actuator configuration will be output. @@ -968,8 +1052,9 @@ ado template context ado template space --from-experiment finetune-gptq-lora-dp-r-4-a-16-tm-default-v1.1.0 ``` - + ##### Creating a template for a space that uses a specific experiment from a specific actuator + ```shell ado template space --from-experiment SFTTrainer:finetune-gptq-lora-dp-r-4-a-16-tm-default-v1.1.0 @@ -1001,19 +1086,33 @@ When required, you can run this command to update all resources of a given kind in the database. ```shell -ado upgrade RESOURCE_TYPE +ado upgrade RESOURCE_TYPE [--apply-legacy-validator ] \ + [--list-legacy-validators] ``` Where: - `RESOURCE_TYPE` is one of the supported resource types: + + - _actuatorconfiguration_ (_ac_) - _datacontainer_ (_dcr_) - _operation_ (_op_) - _samplestore_ (_store_) - _discoveryspace_ (_space_) + + +- `--apply-legacy-validator` applies a specific legacy validator by identifier + during the upgrade process. This option can be specified multiple times to + apply multiple validators. Legacy validators handle deprecated field + migrations and schema transformations. + +- `--list-legacy-validators` lists all available legacy validators for the + specified resource type, showing their identifiers, descriptions, and + deprecated field paths. + #### Examples ##### Upgrade all operation resources @@ -1022,6 +1121,18 @@ Where: ado upgrade operations ``` +##### List available legacy validators for sample stores + +```shell +ado upgrade samplestores --list-legacy-validators +``` + +##### Apply a legacy validator during upgrade + +```shell +ado upgrade samplestores --apply-legacy-validator samplestore_kind_entitysource_to_samplestore +``` + ### ado version When unsure about what ado version you are running, you can get this information @@ -1033,6 +1144,7 @@ ado version ## What's next +
@@ -1055,3 +1167,5 @@ ado version
+ + \ No newline at end of file