Skip to content
Draft
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -26,7 +27,7 @@ def build(self) -> AliasingKuiper:
pass


class BuilderConstraintError(Exception):
class BuilderConstraintError(ToolkitValueError):
pass


Expand Down Expand Up @@ -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):
Expand All @@ -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(
Expand All @@ -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,
)
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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)
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class YamlReadError(Exception):
from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError


class YamlReadError(ToolkitValueError):
def __init__(
self,
message: str,
Expand All @@ -18,7 +21,7 @@ def __init__(
super().__init__(full_message)


class InvalidRuleFormatError(Exception):
class InvalidRuleFormatError(ToolkitValueError):
def __init__(
self,
message: str,
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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")
Expand Down
Loading
Loading