diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/api/facade.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/api/facade.py index 311a23f02f..af6f62c15e 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/api/facade.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/api/facade.py @@ -1,19 +1,21 @@ +from collections.abc import Callable + from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.aliasing_kuiper import AliasingKuiper from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.aliasing_kuiper_builder import ( - AliasingKuiperBuilderFactory, + AliasingKuiperBuilder, AliasingRule, ) class AliasingFacade: - def __init__(self, factory: AliasingKuiperBuilderFactory) -> None: - self._factory = factory + def __init__(self, builder_provider: Callable[[], AliasingKuiperBuilder]) -> None: + self._builder_provider = builder_provider def generate(self, rules: list[AliasingRule]) -> AliasingKuiper: if not rules: raise ValueError("At least one rule must be provided") - builder = self._factory.create() + builder = self._builder_provider() for rule in rules: builder.with_rule(rule) diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/assembly/aliasing_kuiper_builder.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/assembly/aliasing_kuiper_builder.py index 8cd5820cc2..8fa7c52bf2 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/assembly/aliasing_kuiper_builder.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/assembly/aliasing_kuiper_builder.py @@ -1,11 +1,12 @@ from abc import ABC, abstractmethod +from collections import Counter from dataclasses import dataclass from typing import Any from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.aliasing_kuiper import AliasingKuiper from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.expression_composer import ExpressionComposer from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.registry.registry import RuleDefinitionRegistry -from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.composite import ResolvedRuleSpec +from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError @dataclass(frozen=True) @@ -26,7 +27,7 @@ def build(self) -> AliasingKuiper: pass -class BuilderConstraintError(Exception): +class BuilderConstraintError(ToolkitValueError): pass @@ -87,7 +88,6 @@ def _resolve_composite_rules(self, rules: list[AliasingRule]) -> list[AliasingRu if not rules_list: raise ValueError(f"Composite rule '{rule.name}' has empty rules list") - resolved_specs = [] expanded_sub_rules = [] for idx, sub_spec in enumerate(rules_list): @@ -99,9 +99,7 @@ def _resolve_composite_rules(self, rules: list[AliasingRule]) -> list[AliasingRu f"Sub-rule specification {idx} in composite '{rule.name}' must have 'rule_type' and 'payload'" ) - definition = self._registry.get_definition_or_throw(sub_spec["rule_type"]) - resolved_spec = ResolvedRuleSpec(definition=definition, payload=sub_spec["payload"]) - resolved_specs.append(resolved_spec) + self._registry.get_definition_or_throw(sub_spec["rule_type"]) sub_rule_name = f"{rule.name}_sub_{idx}" sub_rule = AliasingRule( @@ -121,25 +119,7 @@ def _validate_rules(self, rules: list[AliasingRule]) -> None: if not rules: raise EmptyRulesError() - rule_names = [rule.name for rule in rules] - duplicates = {name for name in rule_names if rule_names.count(name) > 1} + name_counts = Counter(rule.name for rule in rules) + duplicates = {name for name, count in name_counts.items() if count > 1} if duplicates: raise DuplicateRuleNameError(duplicates) - - -class AliasingKuiperBuilderFactory(ABC): - @abstractmethod - def create(self) -> AliasingKuiperBuilder: - pass - - -class DefaultAliasingKuiperBuilderFactory(AliasingKuiperBuilderFactory): - def __init__(self, registry: RuleDefinitionRegistry, composer: ExpressionComposer) -> None: - self._registry = registry - self._composer = composer - - def create(self) -> AliasingKuiperBuilder: - return DefaultAliasingKuiperBuilder( - registry=self._registry, - composer=self._composer, - ) diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/bootstrap/bootstrapper.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/bootstrap/bootstrapper.py index a121ca5fe9..42f93d921a 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/bootstrap/bootstrapper.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/bootstrap/bootstrapper.py @@ -1,26 +1,19 @@ +from collections.abc import Callable + from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.api.facade import AliasingFacade from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.aliasing_kuiper_builder import ( AliasingKuiperBuilder, - AliasingKuiperBuilderFactory, DefaultAliasingKuiperBuilder, - DefaultAliasingKuiperBuilderFactory, ) from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.expression_composer import ( DefaultExpressionComposer, - ExpressionComposer, ) from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.expression_composition_config import ( AliasingCompositionConfig, OutputProjectionConfig, ) -from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.registry.registry import ( - LocalRuleDefinitionRegistry, - RuleDefinitionRegistry, -) -from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.registry.rules_discovery import ( - LocalRulesDiscovery, - RulesDiscovery, -) +from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.registry.registry import LocalRuleDefinitionRegistry +from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.registry.rules_discovery import LocalRulesDiscovery from cognite_toolkit._cdf_tk.commands.entity_matching.common.json_path import JSONPath @@ -45,52 +38,17 @@ def provide_aliasing_composition_config( ) -def provide_rules_discovery() -> RulesDiscovery: - return LocalRulesDiscovery.create() - - -def provide_rule_definition_registry( - discovery: RulesDiscovery | None = None, -) -> RuleDefinitionRegistry: - resolved_discovery = discovery or provide_rules_discovery() - return LocalRuleDefinitionRegistry.bootstrap(resolved_discovery) - - -def provide_expression_composer( - config: AliasingCompositionConfig | None = None, -) -> ExpressionComposer: - resolved_config = config or provide_aliasing_composition_config() - return DefaultExpressionComposer(resolved_config) - - -def provide_aliasing_kuiper_builder( - registry: RuleDefinitionRegistry | None = None, - composer: ExpressionComposer | None = None, -) -> AliasingKuiperBuilder: - resolved_registry = registry or provide_rule_definition_registry() - resolved_composer = composer or provide_expression_composer() - - return DefaultAliasingKuiperBuilder( - registry=resolved_registry, - composer=resolved_composer, - ) - - -def provide_aliasing_kuiper_builder_factory( - registry: RuleDefinitionRegistry | None = None, - composer: ExpressionComposer | None = None, -) -> AliasingKuiperBuilderFactory: - resolved_registry = registry or provide_rule_definition_registry() - resolved_composer = composer or provide_expression_composer() - - return DefaultAliasingKuiperBuilderFactory( - registry=resolved_registry, - composer=resolved_composer, - ) - - def provide_aliasing_facade( - factory: AliasingKuiperBuilderFactory | None = None, + builder_provider: Callable[[], AliasingKuiperBuilder] | None = None, ) -> AliasingFacade: - resolved_factory = factory or provide_aliasing_kuiper_builder_factory() - return AliasingFacade(resolved_factory) + if builder_provider is None: + registry = LocalRuleDefinitionRegistry.bootstrap(LocalRulesDiscovery.create()) + composer = DefaultExpressionComposer(provide_aliasing_composition_config()) + + def _create_default_builder() -> AliasingKuiperBuilder: + return DefaultAliasingKuiperBuilder(registry=registry, composer=composer) + + resolved_builder_provider = _create_default_builder + else: + resolved_builder_provider = builder_provider + return AliasingFacade(resolved_builder_provider) diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/io/errors.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/io/errors.py index 24ad6d6dec..adaa1bb2b4 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/io/errors.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/io/errors.py @@ -1,4 +1,7 @@ -class YamlReadError(Exception): +from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError + + +class YamlReadError(ToolkitValueError): def __init__( self, message: str, @@ -18,7 +21,7 @@ def __init__( super().__init__(full_message) -class InvalidRuleFormatError(Exception): +class InvalidRuleFormatError(ToolkitValueError): def __init__( self, message: str, diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/io/yaml_rules_reader.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/io/yaml_rules_reader.py index 7a9818d2b4..2e2b251d8c 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/io/yaml_rules_reader.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/io/yaml_rules_reader.py @@ -1,10 +1,12 @@ from dataclasses import dataclass +from pathlib import Path from typing import Any, ClassVar import yaml from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.aliasing_kuiper_builder import AliasingRule from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.io.errors import InvalidRuleFormatError, YamlReadError +from cognite_toolkit._cdf_tk.utils import read_yaml_content, safe_read @dataclass(frozen=True) @@ -19,19 +21,21 @@ class YamlRulesReader: REQUIRED_FIELDS: ClassVar[set[str]] = {"name", "rule_type", "description", "payload"} REQUIRED_ROOT_FIELDS: ClassVar[set[str]] = {"rules", "key_path"} - def read_file(self, file_path: str) -> RulesFileContent: - raw_data = self._load_yaml_file(file_path) - self._validate_root_structure(raw_data, file_path) + def read_file(self, file_path: Path | str) -> RulesFileContent: + resolved_path = Path(file_path) if isinstance(file_path, str) else file_path + file_path_str = resolved_path.as_posix() + raw_data = self._load_yaml_file(resolved_path) + self._validate_root_structure(raw_data, file_path_str) - key_path = self._extract_and_validate_key_path(raw_data, file_path) + key_path = self._extract_and_validate_key_path(raw_data, file_path_str) workflow_id = self._extract_and_validate_optional_string( - raw_data, "workflow_id", "entity_matching_aliasing", file_path + raw_data, "workflow_id", "entity_matching_aliasing", file_path_str ) description = self._extract_and_validate_optional_string( - raw_data, "description", "Entity matching aliasing workflow", file_path + raw_data, "description", "Entity matching aliasing workflow", file_path_str ) rules_data = raw_data.get("rules") - self._validate_rules_is_list(rules_data, file_path) + self._validate_rules_is_list(rules_data, file_path_str) rules: list[AliasingRule] = [] for index, rule_data in enumerate(rules_data): @@ -40,24 +44,23 @@ def read_file(self, file_path: str) -> RulesFileContent: return RulesFileContent(rules=rules, key_path=key_path, workflow_id=workflow_id, description=description) - def _load_yaml_file(self, file_path: str) -> Any: + def _load_yaml_file(self, file_path: Path) -> Any: try: - with open(file_path, encoding="utf-8") as f: - return yaml.safe_load(f) + return read_yaml_content(safe_read(file_path)) except FileNotFoundError as e: raise YamlReadError( "File not found", - file_path=file_path, + file_path=file_path.as_posix(), ) from e except yaml.YAMLError as e: raise YamlReadError( f"Invalid YAML syntax: {e!s}", - file_path=file_path, + file_path=file_path.as_posix(), ) from e except Exception as e: raise YamlReadError( f"Error reading file: {e!s}", - file_path=file_path, + file_path=file_path.as_posix(), ) from e def _validate_root_structure(self, raw_data: Any, file_path: str) -> None: @@ -119,25 +122,6 @@ def _extract_and_validate_optional_string( return value - def _validate_rules_key_exists(self, raw_data: Any, file_path: str) -> None: - if raw_data is None: - raise YamlReadError( - "YAML file is empty or contains only comments", - file_path=file_path, - ) - - if not isinstance(raw_data, dict): - raise YamlReadError( - f"Root of YAML must be a mapping (dictionary), found: {type(raw_data).__name__}", - file_path=file_path, - ) - - if "rules" not in raw_data: - raise YamlReadError( - f"Missing required 'rules' key. Found keys: {list(raw_data.keys())}", - file_path=file_path, - ) - def _validate_rules_is_list(self, rules_data: Any, file_path: str) -> None: if not isinstance(rules_data, list): raise YamlReadError( diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/registry/registry.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/registry/registry.py index 635c41178f..578645c807 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/registry/registry.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/registry/registry.py @@ -6,6 +6,7 @@ RulesDiscovery, ) from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.base import RuleDefinition, RuleType +from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError class RuleDefinitionRegistry(ABC): @@ -14,7 +15,7 @@ def get_definition_or_throw(self, rule_type: RuleType) -> RuleDefinition[Any]: pass -class RuleDefinitionNotFoundError(Exception): +class RuleDefinitionNotFoundError(ToolkitValueError): def __init__(self, rule_type: RuleType) -> None: self.rule_type = rule_type super().__init__(f"Rule type {rule_type.value} not found in registry") diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/registry/rules_discovery.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/registry/rules_discovery.py index af034b71bd..2a5783c596 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/registry/rules_discovery.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/registry/rules_discovery.py @@ -1,13 +1,22 @@ -import importlib -import inspect -import logging from abc import ABC, abstractmethod -from pathlib import Path from typing import Any from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.base import RuleDefinition, RuleType - -logger = logging.getLogger(__name__) +from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.case_transformation import ( + CaseTransformationRuleDefinition, +) +from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.character_substitution import ( + CharacterSubstitutionRuleDefinition, +) +from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.composite import CompositeRuleDefinition +from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.leading_zero_normalization import ( + LeadingZeroNormalizationRuleDefinition, +) +from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.prefix_suffix import PrefixSuffixRuleDefinition +from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.regex_substitution import ( + RegExpSubstitutionRuleDefinition, +) +from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.value_expansion import ValueExpansionRuleDefinition class RulesDiscovery(ABC): @@ -17,68 +26,18 @@ def discover_rules(self) -> dict[RuleType, RuleDefinition[Any]]: class LocalRulesDiscovery(RulesDiscovery): - _MODULE_PATH_PREFIX = "cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules" - - def __init__(self) -> None: - self._rules_dir = self._get_rules_directory() - @staticmethod def create() -> "LocalRulesDiscovery": return LocalRulesDiscovery() def discover_rules(self) -> dict[RuleType, RuleDefinition[Any]]: - rules: dict[RuleType, RuleDefinition[Any]] = {} - - try: - rule_files = self._get_rule_modules() - for module_name in rule_files: - try: - module = importlib.import_module(f"{self._MODULE_PATH_PREFIX}.{module_name}") - discovered_definitions = self._extract_rule_definitions(module) - - for rule_def in discovered_definitions: - try: - instance = rule_def() - rule_type = instance.type() - rules[rule_type] = instance - logger.info(f"Discovered rule definition: {rule_def.__name__} -> {rule_type}") - except Exception as e: - logger.warning(f"Failed to instantiate rule definition {rule_def.__name__}: {e}") - - except ImportError as exception: - logger.warning(f"Failed to import rules module '{module_name}': {exception}") - - except Exception as exception: - logger.error(f"Error during rule discovery: {exception}") - raise RuntimeError(f"Failed to discover rules: {exception}") from exception - - if not rules: - logger.warning("No rule definitions were discovered") - - return rules - - def _get_rules_directory(self) -> Path: - current_file = Path(__file__).resolve() - registry_dir = current_file.parent - aliasing_dir = registry_dir.parent - rules_dir = aliasing_dir / "rules" - return rules_dir - - def _get_rule_modules(self) -> list[str]: - py_files = sorted(self._rules_dir.glob("*.py")) - module_names = [file.stem for file in py_files if file.stem not in ("__init__", "base")] - return module_names - - def _extract_rule_definitions(self, module: Any) -> list[type[RuleDefinition[Any]]]: - rule_definitions: list[type[RuleDefinition[Any]]] = [] - - for _, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and issubclass(obj, RuleDefinition) - and obj is not RuleDefinition - and obj.__module__ == module.__name__ - ): - rule_definitions.append(obj) - - return rule_definitions + definitions: list[RuleDefinition[Any]] = [ + CharacterSubstitutionRuleDefinition(), + RegExpSubstitutionRuleDefinition(), + PrefixSuffixRuleDefinition(), + CaseTransformationRuleDefinition(), + ValueExpansionRuleDefinition(), + LeadingZeroNormalizationRuleDefinition(), + CompositeRuleDefinition(), + ] + return {definition.type(): definition for definition in definitions} diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/base.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/base.py index 6a4532632d..7e69871e22 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/base.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/base.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from dataclasses import dataclass from enum import Enum from typing import Any, Generic, TypeVar @@ -31,40 +30,3 @@ def deserialize_context(self, payload: dict[str, Any]) -> RuleContext: @abstractmethod def create_kuiper_macro(self, context: RuleContext) -> Macro: pass - - -@dataclass(frozen=True) -class RuleName: - name: str - - def __post_init__(self) -> None: - if not self.name: - raise ValueError("Rule name cannot be empty") - - -@dataclass(frozen=True) -class RuleDescription: - description: str - - def __post_init__(self) -> None: - if not self.description: - raise ValueError("Rule description cannot be empty") - - -class Rule: - def __init__(self, name: RuleName, description: RuleDescription, rule_definition: RuleDefinition[Any]) -> None: - self.name: RuleName = name - self.description: RuleDescription = description - self.rule_definition: RuleDefinition[Any] = rule_definition - - @staticmethod - def from_rule_definition( - name: RuleName, description: RuleDescription, rule_definition: RuleDefinition[Any] - ) -> "Rule": - return Rule(name, description, rule_definition) - - def __repr__(self) -> str: - return f"Rule(name={self.name}, description={self.description}, rule_definition={self.rule_definition})" - - def create_kuiper_macro(self, context: Any) -> Macro: - return self.rule_definition.create_kuiper_macro(context) diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/case_transformation.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/case_transformation.py index bdd7bb8f50..e079d8ee3d 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/case_transformation.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/case_transformation.py @@ -21,20 +21,6 @@ def __post_init__(self) -> None: raise ValueError("strategy cannot be empty") -class CaseTransformationContextBuilder: - def __init__(self) -> None: - self._strategy: CaseStrategy | None = None - - def with_strategy(self, strategy: CaseStrategy) -> "CaseTransformationContextBuilder": - self._strategy = strategy - return self - - def build(self) -> CaseTransformationContext: - if self._strategy is None: - raise ValueError("strategy must be set before building") - return CaseTransformationContext(strategy=self._strategy) - - class CaseTransformationRuleDefinition(RuleDefinition[CaseTransformationContext]): def type(self) -> RuleType: return RuleType.CASE_TRANSFORMATION @@ -63,5 +49,7 @@ def create_kuiper_macro(self, context: CaseTransformationContext) -> Macro: expression = f"({var_name}) => {var_name}.map(value => upper(value))" case CaseStrategy.LOWERCASE: expression = f"({var_name}) => {var_name}.map(value => lower(value))" + case _: + raise ValueError(f"Unsupported case transformation strategy: {context.strategy}") return Macro(definition=expression) diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/character_substitution.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/character_substitution.py index d52593c440..0805a04aea 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/character_substitution.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/character_substitution.py @@ -18,18 +18,6 @@ def __post_init__(self) -> None: raise ValueError("from_char cannot be empty") -class CharacterSubstitutionContextBuilder: - def __init__(self) -> None: - self._replacements: dict[str, str] = {} - - def add_replacement(self, from_char: str, to_char: str) -> "CharacterSubstitutionContextBuilder": - self._replacements[from_char] = to_char - return self - - def build(self) -> CharacterSubstitutionContext: - return CharacterSubstitutionContext(self._replacements) - - class CharacterSubstitutionRuleDefinition(RuleDefinition[CharacterSubstitutionContext]): def type(self) -> RuleType: return RuleType.CHARACTER_SUBSTITUTION diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/composite.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/composite.py index 20dbcbca4c..097e6bdb03 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/composite.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/composite.py @@ -24,18 +24,6 @@ def __post_init__(self) -> None: raise ValueError("All items in rules list must be ResolvedRuleSpec instances") -class CompositeRuleContextBuilder: - def __init__(self) -> None: - self._rules: list[ResolvedRuleSpec] = [] - - def add_rule(self, definition: RuleDefinition[Any], payload: dict[str, Any]) -> "CompositeRuleContextBuilder": - self._rules.append(ResolvedRuleSpec(definition=definition, payload=payload)) - return self - - def build(self) -> CompositeRuleContext: - return CompositeRuleContext(rules=self._rules) - - class CompositeRuleDefinition(RuleDefinition[CompositeRuleContext]): def type(self) -> RuleType: return RuleType.COMPOSITE diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/leading_zero_normalization.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/leading_zero_normalization.py index dfc19395fd..4ebac59430 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/leading_zero_normalization.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/leading_zero_normalization.py @@ -15,20 +15,6 @@ def __post_init__(self) -> None: raise ValueError("target_length cannot be negative") -class LeadingZeroNormalizationContextBuilder: - def __init__(self) -> None: - self._target_length: int | None = None - - def with_target_length(self, length: int) -> "LeadingZeroNormalizationContextBuilder": - self._target_length = length - return self - - def build(self) -> LeadingZeroNormalizationContext: - if self._target_length is None: - raise ValueError("target_length must be set before building") - return LeadingZeroNormalizationContext(target_length=self._target_length) - - class LeadingZeroNormalizationRuleDefinition(RuleDefinition[LeadingZeroNormalizationContext]): def type(self) -> RuleType: return RuleType.LEADING_ZERO_NORMALIZATION diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/prefix_suffix.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/prefix_suffix.py index a050041575..767fd9028d 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/prefix_suffix.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/prefix_suffix.py @@ -16,23 +16,6 @@ def __post_init__(self) -> None: raise ValueError("At least one of prefix or suffix must be provided and non-empty") -class PrefixSuffixContextBuilder: - def __init__(self) -> None: - self._prefix: str | None = None - self._suffix: str | None = None - - def with_prefix(self, prefix: str) -> "PrefixSuffixContextBuilder": - self._prefix = prefix - return self - - def with_suffix(self, suffix: str) -> "PrefixSuffixContextBuilder": - self._suffix = suffix - return self - - def build(self) -> PrefixSuffixContext: - return PrefixSuffixContext(prefix=self._prefix, suffix=self._suffix) - - class PrefixSuffixRuleDefinition(RuleDefinition[PrefixSuffixContext]): def type(self) -> RuleType: return RuleType.PREFIX_SUFFIX diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/regex_substitution.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/regex_substitution.py index dc173cb235..6cc340748c 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/regex_substitution.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/regex_substitution.py @@ -18,27 +18,6 @@ def __post_init__(self) -> None: raise ValueError("replacement cannot be empty") -class RegExpSubstitutionContextBuilder: - def __init__(self) -> None: - self._pattern: str | None = None - self._replacement: str | None = None - - def with_pattern(self, pattern: str) -> "RegExpSubstitutionContextBuilder": - self._pattern = pattern - return self - - def with_replacement(self, replacement: str) -> "RegExpSubstitutionContextBuilder": - self._replacement = replacement - return self - - def build(self) -> RegExpSubstitutionContext: - if self._pattern is None: - raise ValueError("pattern must be set before building") - if self._replacement is None: - raise ValueError("replacement must be set before building") - return RegExpSubstitutionContext(pattern=self._pattern, replacement=self._replacement) - - class RegExpSubstitutionRuleDefinition(RuleDefinition[RegExpSubstitutionContext]): def type(self) -> RuleType: return RuleType.REGEX_SUBSTITUTION diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/value_expansion.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/value_expansion.py index 46348f6a34..581fd8acb4 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/value_expansion.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/aliasing/rules/value_expansion.py @@ -26,18 +26,6 @@ def __post_init__(self) -> None: raise ValueError("expansion value cannot be empty") -class ValueExpansionContextBuilder: - def __init__(self) -> None: - self._expansions: dict[str, list[str]] = {} - - def add_expansion(self, abbreviation: str, expansions: list[str]) -> "ValueExpansionContextBuilder": - self._expansions[abbreviation] = expansions - return self - - def build(self) -> ValueExpansionContext: - return ValueExpansionContext(self._expansions) - - class ValueExpansionRuleDefinition(RuleDefinition[ValueExpansionContext]): def type(self) -> RuleType: return RuleType.VALUE_EXPANSION diff --git a/cognite_toolkit/_cdf_tk/commands/entity_matching/entity_matching.py b/cognite_toolkit/_cdf_tk/commands/entity_matching/entity_matching.py index 108d870179..6e0530587f 100644 --- a/cognite_toolkit/_cdf_tk/commands/entity_matching/entity_matching.py +++ b/cognite_toolkit/_cdf_tk/commands/entity_matching/entity_matching.py @@ -12,6 +12,8 @@ WorkflowVersionAssemblyRequest, ) from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.io.yaml_rules_reader import YamlRulesReader +from cognite_toolkit._cdf_tk.exceptions import ToolkitFileNotFoundError +from cognite_toolkit._cdf_tk.utils import safe_write from cognite_toolkit._cdf_tk.utils.module_resolver import ModuleResolver @@ -36,7 +38,7 @@ def generate_aliasing_workflow( organization_dir: Path to the organization directory. """ if not input_yaml.exists(): - raise FileNotFoundError(f"Input file not found: {input_yaml}") + raise ToolkitFileNotFoundError(f"Input file not found: {input_yaml}") module_path = ModuleResolver.get_or_prompt_module_path(organization_dir, module_name) @@ -49,7 +51,7 @@ def generate_aliasing_workflow( workflow_version_path = output_dir / f"{stem}.WorkflowVersion.yaml" rules_reader = YamlRulesReader() - rules_content = rules_reader.read_file(str(input_yaml)) + rules_content = rules_reader.read_file(input_yaml) facade = provide_aliasing_facade() rule_kuiper_pairs = [] @@ -67,10 +69,10 @@ def generate_aliasing_workflow( workflow_description=rules_content.description, ) ) - workflow_path.write_text(bundle.workflow_yaml, encoding="utf-8") + safe_write(workflow_path, bundle.workflow_yaml, encoding="utf-8") self.console(f"Generated {workflow_path.as_posix()}") - workflow_version_path.write_text(bundle.workflow_version_yaml, encoding="utf-8") + safe_write(workflow_version_path, bundle.workflow_version_yaml, encoding="utf-8") self.console(f"Generated {workflow_version_path.as_posix()}") print(Panel(f"Generated 2 files in {output_dir.as_posix()}", title="Success", style="green", expand=False)) diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_api/test_facade.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_api/test_facade.py index e1db76a247..79a15bda58 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_api/test_facade.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_api/test_facade.py @@ -6,19 +6,18 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.aliasing_kuiper import AliasingKuiper from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.aliasing_kuiper_builder import ( AliasingKuiperBuilder, - AliasingKuiperBuilderFactory, AliasingRule, ) class TestAliasingFacade: - def test_when_generate_with_single_rule_then_factory_creates_builder_and_builds(self) -> None: - factory = Mock(spec=AliasingKuiperBuilderFactory) + def test_when_generate_with_single_rule_then_builder_provider_creates_builder_and_builds(self) -> None: + builder_provider = Mock() builder = Mock(spec=AliasingKuiperBuilder) built_kuiper = AliasingKuiper(expression="test_expression") builder.build.return_value = built_kuiper builder.with_rule.return_value = builder - factory.create.return_value = builder + builder_provider.return_value = builder rule = AliasingRule( name="test_rule", @@ -26,22 +25,22 @@ def test_when_generate_with_single_rule_then_factory_creates_builder_and_builds( description="A test rule", payload={"from": "a", "to": "b"}, ) - facade = AliasingFacade(factory) + facade = AliasingFacade(builder_provider) result = facade.generate([rule]) - factory.create.assert_called_once() + builder_provider.assert_called_once() builder.with_rule.assert_called_once_with(rule) builder.build.assert_called_once() assert result == built_kuiper def test_when_generate_with_multiple_rules_then_builder_receives_all_rules_in_order(self) -> None: - factory = Mock(spec=AliasingKuiperBuilderFactory) + builder_provider = Mock() builder = Mock(spec=AliasingKuiperBuilder) built_kuiper = AliasingKuiper(expression="combined_expression") builder.build.return_value = built_kuiper builder.with_rule.return_value = builder - factory.create.return_value = builder + builder_provider.return_value = builder rule1 = AliasingRule( name="rule1", @@ -55,11 +54,11 @@ def test_when_generate_with_multiple_rules_then_builder_receives_all_rules_in_or description="Second rule", payload={"from": "c", "to": "d"}, ) - facade = AliasingFacade(factory) + facade = AliasingFacade(builder_provider) result = facade.generate([rule1, rule2]) - factory.create.assert_called_once() + builder_provider.assert_called_once() expected_calls = [call(rule1), call(rule2)] builder.with_rule.assert_has_calls(expected_calls) assert builder.with_rule.call_count == 2 @@ -67,28 +66,28 @@ def test_when_generate_with_multiple_rules_then_builder_receives_all_rules_in_or assert result == built_kuiper def test_when_generate_with_empty_rules_list_then_raises_value_error(self) -> None: - factory = Mock(spec=AliasingKuiperBuilderFactory) - facade = AliasingFacade(factory) + builder_provider = Mock() + facade = AliasingFacade(builder_provider) with pytest.raises(ValueError, match="At least one rule must be provided"): facade.generate([]) - def test_when_generate_with_empty_rules_list_then_factory_never_called(self) -> None: - factory = Mock(spec=AliasingKuiperBuilderFactory) - facade = AliasingFacade(factory) + def test_when_generate_with_empty_rules_list_then_builder_provider_never_called(self) -> None: + builder_provider = Mock() + facade = AliasingFacade(builder_provider) with pytest.raises(ValueError): facade.generate([]) - factory.create.assert_not_called() + builder_provider.assert_not_called() def test_when_generate_returns_kuiper_with_correct_type(self) -> None: - factory = Mock(spec=AliasingKuiperBuilderFactory) + builder_provider = Mock() builder = Mock(spec=AliasingKuiperBuilder) built_kuiper = AliasingKuiper(expression="(s) => s.replace('a', 'b')") builder.build.return_value = built_kuiper builder.with_rule.return_value = builder - factory.create.return_value = builder + builder_provider.return_value = builder rule = AliasingRule( name="test_rule", @@ -96,15 +95,15 @@ def test_when_generate_returns_kuiper_with_correct_type(self) -> None: description="A test rule", payload={"from": "a", "to": "b"}, ) - facade = AliasingFacade(factory) + facade = AliasingFacade(builder_provider) result = facade.generate([rule]) assert isinstance(result, AliasingKuiper) assert result.expression == "(s) => s.replace('a', 'b')" - def test_when_generate_called_multiple_times_then_factory_creates_fresh_builder_each_call(self) -> None: - factory = Mock(spec=AliasingKuiperBuilderFactory) + def test_when_generate_called_multiple_times_then_builder_provider_creates_fresh_builder_each_call(self) -> None: + builder_provider = Mock() builder1 = Mock(spec=AliasingKuiperBuilder) builder2 = Mock(spec=AliasingKuiperBuilder) kuiper1 = AliasingKuiper(expression="expression1") @@ -113,7 +112,7 @@ def test_when_generate_called_multiple_times_then_factory_creates_fresh_builder_ builder1.with_rule.return_value = builder1 builder2.build.return_value = kuiper2 builder2.with_rule.return_value = builder2 - factory.create.side_effect = [builder1, builder2] + builder_provider.side_effect = [builder1, builder2] rule1 = AliasingRule( name="rule1", @@ -127,11 +126,11 @@ def test_when_generate_called_multiple_times_then_factory_creates_fresh_builder_ description="Rule 2", payload={"from": "c", "to": "d"}, ) - facade = AliasingFacade(factory) + facade = AliasingFacade(builder_provider) result1 = facade.generate([rule1]) result2 = facade.generate([rule2]) - assert factory.create.call_count == 2 + assert builder_provider.call_count == 2 assert result1 == kuiper1 assert result2 == kuiper2 diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_bootstrap/test_bootstrapper.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_bootstrap/test_bootstrapper.py index 9d6d58cb77..53cd6b3021 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_bootstrap/test_bootstrapper.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_bootstrap/test_bootstrapper.py @@ -5,13 +5,7 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.api.facade import AliasingFacade from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.aliasing_kuiper_builder import ( AliasingKuiperBuilder, - AliasingKuiperBuilderFactory, - DefaultAliasingKuiperBuilder, - DefaultAliasingKuiperBuilderFactory, -) -from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.expression_composer import ( - DefaultExpressionComposer, - ExpressionComposer, + AliasingRule, ) from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.assembly.expression_composition_config import ( AliasingCompositionConfig, @@ -20,21 +14,8 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.bootstrap.bootstrapper import ( provide_aliasing_composition_config, provide_aliasing_facade, - provide_aliasing_kuiper_builder, - provide_aliasing_kuiper_builder_factory, - provide_expression_composer, provide_json_path, provide_output_projection_config, - provide_rule_definition_registry, - provide_rules_discovery, -) -from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.registry.registry import ( - LocalRuleDefinitionRegistry, - RuleDefinitionRegistry, -) -from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.registry.rules_discovery import ( - LocalRulesDiscovery, - RulesDiscovery, ) from cognite_toolkit._cdf_tk.commands.entity_matching.common.json_path import InvalidJSONPathError, JSONPath @@ -115,208 +96,28 @@ def test_each_call_creates_new_instance(self) -> None: assert result1 is not result2 -class TestProvideRulesDiscovery: - def test_returns_rules_discovery_interface(self) -> None: - result = provide_rules_discovery() - assert isinstance(result, RulesDiscovery) - - def test_returns_local_rules_discovery_implementation(self) -> None: - result = provide_rules_discovery() - assert isinstance(result, LocalRulesDiscovery) - - def test_each_call_creates_new_instance(self) -> None: - result1 = provide_rules_discovery() - result2 = provide_rules_discovery() - assert result1 is not result2 - - def test_discovery_can_discover_rules(self) -> None: - result = provide_rules_discovery() - discovered = result.discover_rules() - assert isinstance(discovered, dict) - - -class TestProvideRuleDefinitionRegistry: - def test_with_no_args_returns_registry_with_discovered_rules(self) -> None: - result = provide_rule_definition_registry() - assert isinstance(result, RuleDefinitionRegistry) - assert isinstance(result, LocalRuleDefinitionRegistry) - - def test_with_custom_discovery_uses_provided_discovery(self) -> None: - mock_discovery = Mock(spec=RulesDiscovery) - mock_discovery.discover_rules.return_value = {} - - result = provide_rule_definition_registry(discovery=mock_discovery) - assert isinstance(result, RuleDefinitionRegistry) - mock_discovery.discover_rules.assert_called_once() - - def test_default_registry_has_character_substitution_rule(self) -> None: - result = provide_rule_definition_registry() - rules = result.get_definition_or_throw("character_substitution") # type: ignore[arg-type] - assert rules is not None - - def test_each_call_creates_new_instance(self) -> None: - result1 = provide_rule_definition_registry() - result2 = provide_rule_definition_registry() - assert result1 is not result2 - - def test_with_custom_discovery_each_call_creates_new_registry(self) -> None: - mock_discovery = Mock(spec=RulesDiscovery) - mock_discovery.discover_rules.return_value = {} - - result1 = provide_rule_definition_registry(discovery=mock_discovery) - result2 = provide_rule_definition_registry(discovery=mock_discovery) - assert result1 is not result2 - - -class TestProvideExpressionComposer: - def test_with_no_args_returns_composer_with_defaults(self) -> None: - result = provide_expression_composer() - assert isinstance(result, ExpressionComposer) - assert isinstance(result, DefaultExpressionComposer) - - def test_with_custom_config_uses_provided_config(self) -> None: - custom_config = provide_aliasing_composition_config() - result = provide_expression_composer(config=custom_config) - assert isinstance(result, ExpressionComposer) - assert result._config is custom_config # type: ignore[attr-defined] - - def test_default_composer_has_composition_config(self) -> None: - result = provide_expression_composer() - assert result._config is not None # type: ignore[attr-defined] - assert isinstance(result._config, AliasingCompositionConfig) # type: ignore[attr-defined] - - def test_each_call_creates_new_instance(self) -> None: - result1 = provide_expression_composer() - result2 = provide_expression_composer() - assert result1 is not result2 - - -class TestProvideAliasingKuiperBuilder: - def test_with_no_args_returns_builder_with_auto_resolved_deps(self) -> None: - result = provide_aliasing_kuiper_builder() - assert isinstance(result, AliasingKuiperBuilder) - assert isinstance(result, DefaultAliasingKuiperBuilder) - - def test_with_custom_registry_uses_provided_registry(self) -> None: - mock_registry = Mock(spec=RuleDefinitionRegistry) - result = provide_aliasing_kuiper_builder(registry=mock_registry) - assert result._registry is mock_registry # type: ignore[attr-defined] - - def test_with_custom_composer_uses_provided_composer(self) -> None: - mock_composer = Mock(spec=ExpressionComposer) - result = provide_aliasing_kuiper_builder(composer=mock_composer) - assert result._composer is mock_composer # type: ignore[attr-defined] - - def test_with_both_custom_deps_uses_all(self) -> None: - mock_registry = Mock(spec=RuleDefinitionRegistry) - mock_composer = Mock(spec=ExpressionComposer) - result = provide_aliasing_kuiper_builder( - registry=mock_registry, - composer=mock_composer, - ) - assert result._registry is mock_registry # type: ignore[attr-defined] - assert result._composer is mock_composer # type: ignore[attr-defined] - - def test_default_builder_resolves_all_dependencies(self) -> None: - result = provide_aliasing_kuiper_builder() - assert result._registry is not None # type: ignore[attr-defined] - assert result._composer is not None # type: ignore[attr-defined] - assert isinstance(result._registry, RuleDefinitionRegistry) # type: ignore[attr-defined] - assert isinstance(result._composer, ExpressionComposer) # type: ignore[attr-defined] - - def test_each_call_creates_new_instance(self) -> None: - result1 = provide_aliasing_kuiper_builder() - result2 = provide_aliasing_kuiper_builder() - assert result1 is not result2 - - def test_each_call_creates_new_dependencies(self) -> None: - result1 = provide_aliasing_kuiper_builder() - result2 = provide_aliasing_kuiper_builder() - assert result1._registry is not result2._registry # type: ignore[attr-defined] - assert result1._composer is not result2._composer # type: ignore[attr-defined] - - class TestProvideAliasingFacade: - def test_with_no_args_returns_facade_with_auto_resolved_factory(self) -> None: + def test_with_no_args_returns_facade(self) -> None: result = provide_aliasing_facade() assert isinstance(result, AliasingFacade) - def test_with_custom_factory_uses_provided_factory(self) -> None: - mock_factory = Mock(spec=AliasingKuiperBuilderFactory) - result = provide_aliasing_facade(factory=mock_factory) - assert result._factory is mock_factory - - def test_default_facade_has_factory(self) -> None: - result = provide_aliasing_facade() - assert result._factory is not None - assert isinstance(result._factory, AliasingKuiperBuilderFactory) - - def test_each_call_creates_new_instance(self) -> None: - result1 = provide_aliasing_facade() - result2 = provide_aliasing_facade() - assert result1 is not result2 - - def test_each_call_creates_new_factory(self) -> None: - result1 = provide_aliasing_facade() - result2 = provide_aliasing_facade() - assert result1._factory is not result2._factory + def test_with_custom_builder_provider_uses_provided_builder_provider(self) -> None: + mock_builder_provider = Mock(return_value=Mock(spec=AliasingKuiperBuilder)) + result = provide_aliasing_facade(builder_provider=mock_builder_provider) + assert result._builder_provider is mock_builder_provider - -class TestBootstrapperDependencyComposition: - def test_full_dependency_chain_with_defaults(self) -> None: + def test_default_facade_generates_expression_for_known_rule(self) -> None: facade = provide_aliasing_facade() - assert isinstance(facade, AliasingFacade) - assert isinstance(facade._factory, AliasingKuiperBuilderFactory) - assert isinstance(facade._factory, DefaultAliasingKuiperBuilderFactory) - - def test_override_at_factory_level_propagates_to_facade(self) -> None: - custom_composer = Mock(spec=ExpressionComposer) - custom_registry = Mock(spec=RuleDefinitionRegistry) - custom_factory = DefaultAliasingKuiperBuilderFactory( - registry=custom_registry, - composer=custom_composer, + kuiper = facade.generate( + [ + AliasingRule( + name="test_rule", + rule_type="character_substitution", + description="Test rule", + payload={"replacements": {"P": "K"}}, + ) + ] ) - facade = provide_aliasing_facade(factory=custom_factory) - - assert facade._factory is custom_factory - builder = facade._factory.create() - assert builder._composer is custom_composer # type: ignore[attr-defined] - assert builder._registry is custom_registry # type: ignore[attr-defined] - - def test_multiple_facade_instances_are_independent(self) -> None: - facade1 = provide_aliasing_facade() - facade2 = provide_aliasing_facade() - - assert facade1 is not facade2 - assert facade1._factory is not facade2._factory - - def test_factory_creates_fresh_builders(self) -> None: - factory = provide_aliasing_kuiper_builder_factory() - builder1 = factory.create() - builder2 = factory.create() - - assert builder1 is not builder2 - assert isinstance(builder1, DefaultAliasingKuiperBuilder) - assert isinstance(builder2, DefaultAliasingKuiperBuilder) - def test_all_factories_return_interfaces_not_implementation_types(self) -> None: - json_path = provide_json_path() - assert type(json_path).__name__ == "JSONPath" - - discovery = provide_rules_discovery() - assert isinstance(discovery, RulesDiscovery) - - registry = provide_rule_definition_registry() - assert isinstance(registry, RuleDefinitionRegistry) - - composer = provide_expression_composer() - assert isinstance(composer, ExpressionComposer) - - builder = provide_aliasing_kuiper_builder() - assert isinstance(builder, AliasingKuiperBuilder) - - factory = provide_aliasing_kuiper_builder_factory() - assert isinstance(factory, AliasingKuiperBuilderFactory) - - facade = provide_aliasing_facade() - assert isinstance(facade, AliasingFacade) + assert kuiper.expression + assert "map(entity =>" in kuiper.expression diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_base.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_base.py index 64a3107d12..69199c5434 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_base.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_base.py @@ -1,97 +1,8 @@ -from typing import Any -from unittest.mock import Mock - -import pytest - from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.base import ( - Rule, - RuleDefinition, - RuleDescription, - RuleName, RuleType, ) -from cognite_toolkit._cdf_tk.commands.entity_matching.common.macro import Macro class TestRuleType: def test_enum_value_is_correct(self) -> None: assert RuleType.CHARACTER_SUBSTITUTION == "character_substitution" - - -class TestRuleName: - def test_when_valid_name_then_creation_succeeds(self) -> None: - rule_name = RuleName("my_rule") - assert rule_name.name == "my_rule" - - def test_when_empty_name_then_raises_value_error(self) -> None: - with pytest.raises(ValueError, match="Rule name cannot be empty"): - RuleName("") - - def test_when_none_name_then_raises_value_error(self) -> None: - with pytest.raises(ValueError, match="Rule name cannot be empty"): - RuleName(None) # type: ignore[arg-type] - - -class TestRuleDescription: - def test_when_valid_description_then_creation_succeeds(self) -> None: - desc = RuleDescription("This is a test rule") - assert desc.description == "This is a test rule" - - def test_when_empty_description_then_raises_value_error(self) -> None: - with pytest.raises(ValueError, match="Rule description cannot be empty"): - RuleDescription("") - - def test_when_none_description_then_raises_value_error(self) -> None: - with pytest.raises(ValueError, match="Rule description cannot be empty"): - RuleDescription(None) # type: ignore[arg-type] - - -class MockRuleDefinition(RuleDefinition): - def __init__(self, rule_type: RuleType, macro: Macro): - self._rule_type = rule_type - self._macro = macro - - def type(self) -> RuleType: - return self._rule_type - - def deserialize_context(self, payload: dict[str, Any]) -> Any: - return {} - - def create_kuiper_macro(self, context: Any) -> Macro: - return self._macro - - -class TestRule: - def test_when_valid_components_then_creation_succeeds(self) -> None: - name = RuleName("test_rule") - description = RuleDescription("A test rule") - macro = Macro(definition="test_def") - rule_def = MockRuleDefinition(RuleType.CHARACTER_SUBSTITUTION, macro) - - rule = Rule(name, description, rule_def) - assert rule.name == name - assert rule.description == description - assert rule.rule_definition == rule_def - - def test_when_using_factory_method_then_rule_created(self) -> None: - name = RuleName("test_rule") - description = RuleDescription("A test rule") - macro = Macro(definition="test_def") - rule_def = MockRuleDefinition(RuleType.CHARACTER_SUBSTITUTION, macro) - - rule = Rule.from_rule_definition(name, description, rule_def) - assert rule.name == name - assert rule.description == description - assert rule.rule_definition == rule_def - - def test_when_calling_create_kuiper_macro_then_returns_macro(self) -> None: - name = RuleName("test_rule") - description = RuleDescription("A test rule") - expected_macro = Macro(definition="(s) => s.replace('a', 'b')") - rule_def = MockRuleDefinition(RuleType.CHARACTER_SUBSTITUTION, expected_macro) - rule = Rule(name, description, rule_def) - - context = Mock() - result = rule.create_kuiper_macro(context) - - assert result == expected_macro diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_case_transformation.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_case_transformation.py index d65fffa0cb..8de04f1ff9 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_case_transformation.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_case_transformation.py @@ -6,7 +6,6 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.case_transformation import ( CaseStrategy, CaseTransformationContext, - CaseTransformationContextBuilder, CaseTransformationRuleDefinition, ) from cognite_toolkit._cdf_tk.commands.entity_matching.common.macro import MacroCallSignature @@ -38,34 +37,6 @@ def test_when_lowercase_strategy_then_creation_succeeds(self) -> None: assert context.strategy == CaseStrategy.LOWERCASE -class TestCaseTransformationContextBuilder: - def test_when_building_with_uppercase_then_context_created(self) -> None: - builder = CaseTransformationContextBuilder() - context = builder.with_strategy(CaseStrategy.UPPERCASE).build() - assert context.strategy == CaseStrategy.UPPERCASE - - def test_when_building_with_lowercase_then_context_created(self) -> None: - builder = CaseTransformationContextBuilder() - context = builder.with_strategy(CaseStrategy.LOWERCASE).build() - assert context.strategy == CaseStrategy.LOWERCASE - - def test_when_building_without_strategy_then_raises_value_error(self) -> None: - builder = CaseTransformationContextBuilder() - with pytest.raises(ValueError, match="strategy must be set before building"): - builder.build() - - def test_when_builder_returns_self_then_fluent_chaining_works(self) -> None: - builder = CaseTransformationContextBuilder() - result = builder.with_strategy(CaseStrategy.UPPERCASE) - assert isinstance(result, CaseTransformationContextBuilder) - assert result is builder - - def test_when_overriding_strategy_then_latest_value_used(self) -> None: - builder = CaseTransformationContextBuilder() - context = builder.with_strategy(CaseStrategy.UPPERCASE).with_strategy(CaseStrategy.LOWERCASE).build() - assert context.strategy == CaseStrategy.LOWERCASE - - class TestCaseTransformationRuleDefinition: def test_when_calling_type_then_returns_correct_rule_type(self) -> None: rule_def = CaseTransformationRuleDefinition() diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_character_substitution.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_character_substitution.py index b8fccd53b2..191b489909 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_character_substitution.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_character_substitution.py @@ -5,7 +5,6 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.base import RuleType from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.character_substitution import ( CharacterSubstitutionContext, - CharacterSubstitutionContextBuilder, CharacterSubstitutionRuleDefinition, ) from cognite_toolkit._cdf_tk.commands.entity_matching.common.macro import MacroCallSignature @@ -35,34 +34,6 @@ def test_when_empty_from_char_then_raises_value_error(self) -> None: CharacterSubstitutionContext({"": "b"}) -class TestCharacterSubstitutionContextBuilder: - def test_when_building_with_single_replacement_then_context_created(self) -> None: - builder = CharacterSubstitutionContextBuilder() - context = builder.add_replacement("a", "b").build() - assert context.replacements == {"a": "b"} - - def test_when_building_with_multiple_replacements_then_all_added(self) -> None: - builder = CharacterSubstitutionContextBuilder() - context = builder.add_replacement("a", "b").add_replacement("c", "d").add_replacement("e", "f").build() - assert context.replacements == {"a": "b", "c": "d", "e": "f"} - - def test_when_overriding_replacement_then_latest_value_used(self) -> None: - builder = CharacterSubstitutionContextBuilder() - context = builder.add_replacement("a", "b").add_replacement("a", "c").build() - assert context.replacements == {"a": "c"} - - def test_when_building_without_replacements_then_raises_value_error(self) -> None: - builder = CharacterSubstitutionContextBuilder() - with pytest.raises(ValueError, match="replacements dictionary cannot be empty"): - builder.build() - - def test_when_builder_returns_self_then_fluent_chaining_works(self) -> None: - builder = CharacterSubstitutionContextBuilder() - result = builder.add_replacement("a", "b") - assert isinstance(result, CharacterSubstitutionContextBuilder) - assert result is builder - - class TestCharacterSubstitutionRuleDefinition: def test_when_calling_type_then_returns_correct_rule_type(self) -> None: rule_def = CharacterSubstitutionRuleDefinition() diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_composite.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_composite.py index 6844d4cb03..482d31985e 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_composite.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_composite.py @@ -5,7 +5,6 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.base import RuleDefinition, RuleType from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.composite import ( CompositeRuleContext, - CompositeRuleContextBuilder, CompositeRuleDefinition, ResolvedRuleSpec, ) @@ -49,48 +48,6 @@ def test_when_non_resolved_spec_item_then_raises_value_error(self) -> None: CompositeRuleContext(rules=[{"definition": Mock()}]) # type: ignore[list-item] -class TestCompositeRuleContextBuilder: - def test_when_adding_single_rule_then_context_created(self) -> None: - mock_def = Mock(spec=RuleDefinition) - builder = CompositeRuleContextBuilder() - context = builder.add_rule(mock_def, {"prefix": "P"}).build() - - assert len(context.rules) == 1 - assert context.rules[0].definition is mock_def - - def test_when_adding_multiple_rules_then_all_added(self) -> None: - mock_def1 = Mock(spec=RuleDefinition) - mock_def2 = Mock(spec=RuleDefinition) - mock_def3 = Mock(spec=RuleDefinition) - - builder = CompositeRuleContextBuilder() - context = ( - builder.add_rule(mock_def1, {"prefix": "P"}) - .add_rule(mock_def2, {"case": "upper"}) - .add_rule(mock_def3, {"expansions": {"A": ["B"]}}) - .build() - ) - - assert len(context.rules) == 3 - assert context.rules[0].definition is mock_def1 - assert context.rules[1].definition is mock_def2 - assert context.rules[2].definition is mock_def3 - - def test_when_builder_returns_self_then_fluent_chaining_works(self) -> None: - mock_def = Mock(spec=RuleDefinition) - builder = CompositeRuleContextBuilder() - result = builder.add_rule(mock_def, {"prefix": "P"}) - - assert isinstance(result, CompositeRuleContextBuilder) - assert result is builder - - def test_when_building_without_rules_then_raises_value_error(self) -> None: - builder = CompositeRuleContextBuilder() - - with pytest.raises(ValueError, match="At least one rule must be specified"): - builder.build() - - class TestCompositeRuleDefinition: def test_when_calling_type_then_returns_composite_rule_type(self) -> None: rule_def = CompositeRuleDefinition() diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_leading_zero_normalization.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_leading_zero_normalization.py index 3935a7ce6f..ccd44f48e9 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_leading_zero_normalization.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_leading_zero_normalization.py @@ -3,7 +3,6 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.base import RuleType from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.leading_zero_normalization import ( LeadingZeroNormalizationContext, - LeadingZeroNormalizationContextBuilder, LeadingZeroNormalizationRuleDefinition, ) from cognite_toolkit._cdf_tk.commands.entity_matching.common.macro import Macro, MacroCallSignature @@ -31,39 +30,6 @@ def test_when_target_length_is_none_then_raises_type_error(self) -> None: LeadingZeroNormalizationContext(target_length=None) # type: ignore[arg-type] -class TestLeadingZeroNormalizationContextBuilder: - def test_when_building_with_target_length_then_context_created(self) -> None: - builder = LeadingZeroNormalizationContextBuilder() - context = builder.with_target_length(5).build() - assert context.target_length == 5 - - def test_when_building_with_zero_target_length_then_context_created(self) -> None: - builder = LeadingZeroNormalizationContextBuilder() - context = builder.with_target_length(0).build() - assert context.target_length == 0 - - def test_when_building_with_large_target_length_then_context_created(self) -> None: - builder = LeadingZeroNormalizationContextBuilder() - context = builder.with_target_length(50).build() - assert context.target_length == 50 - - def test_when_building_without_target_length_then_raises_value_error(self) -> None: - builder = LeadingZeroNormalizationContextBuilder() - with pytest.raises(ValueError, match="target_length must be set before building"): - builder.build() - - def test_when_builder_returns_self_then_fluent_chaining_works(self) -> None: - builder = LeadingZeroNormalizationContextBuilder() - result = builder.with_target_length(5) - assert isinstance(result, LeadingZeroNormalizationContextBuilder) - assert result is builder - - def test_when_overriding_target_length_then_latest_value_used(self) -> None: - builder = LeadingZeroNormalizationContextBuilder() - context = builder.with_target_length(3).with_target_length(7).build() - assert context.target_length == 7 - - class TestLeadingZeroNormalizationRuleDefinition: def test_when_calling_type_then_returns_correct_rule_type(self) -> None: rule_def = LeadingZeroNormalizationRuleDefinition() diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_prefix_suffix.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_prefix_suffix.py index c798a4b3c4..2718e1bdcb 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_prefix_suffix.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_prefix_suffix.py @@ -5,7 +5,6 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.base import RuleType from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.prefix_suffix import ( PrefixSuffixContext, - PrefixSuffixContextBuilder, PrefixSuffixRuleDefinition, ) from cognite_toolkit._cdf_tk.commands.entity_matching.common.macro import MacroCallSignature @@ -48,47 +47,6 @@ def test_when_prefix_none_and_suffix_empty_then_raises_value_error(self) -> None PrefixSuffixContext(prefix=None, suffix="") -class TestPrefixSuffixContextBuilder: - def test_when_building_with_prefix_only_then_context_created(self) -> None: - builder = PrefixSuffixContextBuilder() - context = builder.with_prefix("PRE_").build() - assert context.prefix == "PRE_" - assert context.suffix is None - - def test_when_building_with_suffix_only_then_context_created(self) -> None: - builder = PrefixSuffixContextBuilder() - context = builder.with_suffix("_SUF").build() - assert context.prefix is None - assert context.suffix == "_SUF" - - def test_when_building_with_both_prefix_and_suffix_then_context_created(self) -> None: - builder = PrefixSuffixContextBuilder() - context = builder.with_prefix("PRE_").with_suffix("_SUF").build() - assert context.prefix == "PRE_" - assert context.suffix == "_SUF" - - def test_when_building_without_prefix_or_suffix_then_raises_value_error(self) -> None: - builder = PrefixSuffixContextBuilder() - with pytest.raises(ValueError, match="At least one of prefix or suffix must be provided and non-empty"): - builder.build() - - def test_when_builder_returns_self_then_fluent_chaining_works(self) -> None: - builder = PrefixSuffixContextBuilder() - result = builder.with_prefix("PRE_") - assert isinstance(result, PrefixSuffixContextBuilder) - assert result is builder - - def test_when_overriding_prefix_then_latest_value_used(self) -> None: - builder = PrefixSuffixContextBuilder() - context = builder.with_prefix("OLD_").with_prefix("NEW_").build() - assert context.prefix == "NEW_" - - def test_when_overriding_suffix_then_latest_value_used(self) -> None: - builder = PrefixSuffixContextBuilder() - context = builder.with_suffix("_OLD").with_suffix("_NEW").build() - assert context.suffix == "_NEW" - - class TestPrefixSuffixRuleDefinition: def test_when_calling_type_then_returns_correct_rule_type(self) -> None: rule_def = PrefixSuffixRuleDefinition() diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_regex_substitution.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_regex_substitution.py index 344b53ca5b..41038c9d7b 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_regex_substitution.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_regex_substitution.py @@ -5,7 +5,6 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.base import RuleType from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.regex_substitution import ( RegExpSubstitutionContext, - RegExpSubstitutionContextBuilder, RegExpSubstitutionRuleDefinition, ) from cognite_toolkit._cdf_tk.commands.entity_matching.common.macro import MacroCallSignature @@ -44,40 +43,6 @@ def test_when_special_regex_characters_in_pattern_then_creation_succeeds(self) - assert context.replacement == replacement -class TestRegExpSubstitutionContextBuilder: - def test_when_building_with_pattern_and_replacement_then_context_created(self) -> None: - builder = RegExpSubstitutionContextBuilder() - context = builder.with_pattern("test").with_replacement("result").build() - assert context.pattern == "test" - assert context.replacement == "result" - - def test_when_builder_returns_self_then_fluent_chaining_works(self) -> None: - builder = RegExpSubstitutionContextBuilder() - result = builder.with_pattern("test") - assert isinstance(result, RegExpSubstitutionContextBuilder) - assert result is builder - - def test_when_building_without_pattern_then_raises_value_error(self) -> None: - builder = RegExpSubstitutionContextBuilder() - with pytest.raises(ValueError, match="pattern must be set before building"): - builder.with_replacement("result").build() - - def test_when_building_without_replacement_then_raises_value_error(self) -> None: - builder = RegExpSubstitutionContextBuilder() - with pytest.raises(ValueError, match="replacement must be set before building"): - builder.with_pattern("test").build() - - def test_when_overriding_pattern_then_latest_value_used(self) -> None: - builder = RegExpSubstitutionContextBuilder() - context = builder.with_pattern("first").with_pattern("second").with_replacement("result").build() - assert context.pattern == "second" - - def test_when_overriding_replacement_then_latest_value_used(self) -> None: - builder = RegExpSubstitutionContextBuilder() - context = builder.with_pattern("test").with_replacement("first").with_replacement("second").build() - assert context.replacement == "second" - - class TestRegExpSubstitutionRuleDefinition: def test_when_calling_type_then_returns_correct_rule_type(self) -> None: rule_def = RegExpSubstitutionRuleDefinition() diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_value_expansion.py b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_value_expansion.py index 64b704a649..b0093d8af7 100644 --- a/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_value_expansion.py +++ b/tests/test_unit/test_cdf_tk/test_commands/test_entity_matching/test_aliasing/test_rules/test_value_expansion.py @@ -3,7 +3,6 @@ from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.base import RuleType from cognite_toolkit._cdf_tk.commands.entity_matching.aliasing.rules.value_expansion import ( ValueExpansionContext, - ValueExpansionContextBuilder, ValueExpansionRuleDefinition, ) from cognite_toolkit._cdf_tk.commands.entity_matching.common.macro import Macro, MacroCallSignature @@ -50,43 +49,6 @@ def test_when_expansion_is_dict_then_raises_value_error(self) -> None: ValueExpansionContext({"P": {"pump": 1}}) # type: ignore[dict-item] -class TestValueExpansionContextBuilder: - def test_when_building_with_single_expansion_then_context_created(self) -> None: - builder = ValueExpansionContextBuilder() - context = builder.add_expansion("P", ["PUMP", "PMP"]).build() - assert context.expansions == {"P": ["PUMP", "PMP"]} - - def test_when_building_with_multiple_expansions_then_all_added(self) -> None: - builder = ValueExpansionContextBuilder() - context = ( - builder.add_expansion("P", ["PUMP", "PMP"]) - .add_expansion("M", ["MOTOR", "MOT"]) - .add_expansion("V", ["VALVE"]) - .build() - ) - assert context.expansions == { - "P": ["PUMP", "PMP"], - "M": ["MOTOR", "MOT"], - "V": ["VALVE"], - } - - def test_when_overriding_expansion_then_latest_value_used(self) -> None: - builder = ValueExpansionContextBuilder() - context = builder.add_expansion("P", ["PUMP"]).add_expansion("P", ["PUMP", "PMP"]).build() - assert context.expansions == {"P": ["PUMP", "PMP"]} - - def test_when_building_without_expansions_then_raises_value_error(self) -> None: - builder = ValueExpansionContextBuilder() - with pytest.raises(ValueError, match="expansions dictionary cannot be empty"): - builder.build() - - def test_when_builder_returns_self_then_fluent_chaining_works(self) -> None: - builder = ValueExpansionContextBuilder() - result = builder.add_expansion("P", ["PUMP"]) - assert isinstance(result, ValueExpansionContextBuilder) - assert result is builder - - class TestValueExpansionRuleDefinition: def test_when_calling_type_then_returns_correct_rule_type(self) -> None: rule_def = ValueExpansionRuleDefinition()