Skip to content

Commit 4d1c571

Browse files
committed
fix: initial draft for storing entity hierarchy
1 parent 121bf02 commit 4d1c571

4 files changed

Lines changed: 337 additions & 6 deletions

File tree

‎src/dve/core_engine/backends/metadata/contract.py‎

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,61 @@
11
"""Metadata classes for the data contract."""
22

3-
from typing import Any
3+
from typing import Any, Optional, Union
44

5-
from pydantic import BaseModel, PrivateAttr, model_validator
5+
from pydantic import BaseModel, Field, PrivateAttr, model_validator
66

77
from dve.core_engine.type_hints import EntityName, ReportingFields
88
from dve.core_engine.validation import RowValidator
9+
from dve.metadata_parser.exc import EntityNotFoundError
910
from dve.parser.type_hints import Extension
1011

12+
class HierarchyNode(BaseModel):
13+
entity_name: str
14+
children: Optional[list["HierarchyNode"]] = Field(default_factory=list)
15+
16+
def get_descendents(self) -> list[str]:
17+
"""Recursively list all descendents of the node"""
18+
descendents = []
19+
for node in self.children:
20+
descendents.append(node.entity_name)
21+
descendents.extend(node.get_descendents())
22+
return descendents
23+
24+
def get_node(self, entity_name:str) -> Union["HierarchyNode", None]:
25+
"""Recursively search for node and return if found"""
26+
node = None
27+
if self.entity_name == entity_name:
28+
return self
29+
else:
30+
for child in self.children:
31+
node = child.get_node(entity_name)
32+
if node:
33+
break
34+
return node
35+
36+
def add_child_node(self, parent_entity: str, child_info: "HierarchyNode") -> None:
37+
"""Add a child node if the parent exists in the hierarchy"""
38+
try:
39+
self.get_node(parent_entity).children.append(child_info)
40+
except AttributeError:
41+
raise EntityNotFoundError(f"Can't find parent node {parent_entity} in {self.entity_name}")
42+
43+
def as_dict(self):
44+
ret_dict = {}
45+
for node in self.children:
46+
ret_dict.update(node.as_dict())
47+
return {self.entity_name: {"children": ret_dict}}
48+
49+
50+
class ChildHierarchyNode(HierarchyNode):
51+
join_fields: list[str]
52+
53+
def as_dict(self):
54+
ret_value = {self.entity_name: {"join_fields": self.join_fields}}
55+
for node in self.children:
56+
ret_value[self.entity_name] |= {"children": node.as_dict()}
57+
return ret_value
58+
1159

1260
class ReaderConfig(BaseModel):
1361
"""Configuration options for a given reader."""
@@ -38,6 +86,7 @@ class DataContractMetadata(BaseModel, frozen=True, arbitrary_types_allowed=True)
3886
"""Whether to cache the original entities after loading."""
3987
_schemas: dict[EntityName, type[BaseModel]] = PrivateAttr(default_factory=dict)
4088
"""The pydantic models of the schmas."""
89+
linkage_hierarchy: dict[EntityName, HierarchyNode] = Field(default_factor=dict)
4190

4291
@property
4392
def schemas(self) -> dict[EntityName, type[BaseModel]]:

‎src/dve/core_engine/configuration/v1/__init__.py‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""The loader for the first JSON-based dataset configuration."""
22

33
import json
4-
from typing import Any, Optional, Union
4+
from typing import Any, Optional, Type, Union
55

66
from pydantic import BaseModel, Field, PrivateAttr, validate_call
77
from typing_extensions import Literal
88

99
from dve.core_engine.backends.base.reference_data import ReferenceConfig, ReferenceConfigUnion
10-
from dve.core_engine.backends.metadata.contract import DataContractMetadata, ReaderConfig
10+
from dve.core_engine.backends.metadata.contract import ChildHierarchyNode, DataContractMetadata, HierarchyNode, ReaderConfig
1111
from dve.core_engine.backends.metadata.rules import AbstractStep, Rule, RuleMetadata
1212
from dve.core_engine.configuration.base import BaseEngineConfig
1313
from dve.core_engine.configuration.v1.filters import (
@@ -24,6 +24,7 @@
2424
from dve.core_engine.message import DataContractErrorDetail
2525
from dve.core_engine.type_hints import EntityName, ErrorCategory, ErrorType, TemplateVariables
2626
from dve.core_engine.validation import RowValidator
27+
from dve.metadata_parser.exc import EntityNotFoundError
2728
from dve.parser.file_handling import joinuri, open_stream, resolve_location
2829
from dve.parser.type_hints import URI, Extension
2930

@@ -38,6 +39,8 @@
3839

3940
FieldName = str
4041
"""The name of a field within a model/schema."""
42+
JoinFields = Optional[list[str]]
43+
"""The fields required to join a child entity back to the parent"""
4144
TypeOrDef = Union[ # pylint: disable=C0103
4245
TypeName, "_CallableTypeDefinition", "_ModelTypeDefinition", "_TypeAliasDefinition"
4346
]
@@ -81,6 +84,14 @@ class _TypeAliasDefinition(_BaseTypeDefintion):
8184
"""The name of the Python type."""
8285

8386

87+
class _LinkageConfig(BaseModel):
88+
"""Specify how to link entities back to parents if required"""
89+
parent_entity: EntityName
90+
"""The name of the parent entity"""
91+
join_fields: JoinFields
92+
"""The fields that can be used to link back to the parent entity"""
93+
94+
8495
class _SchemaConfig(BaseModel):
8596
"""Configuration for a component schema within a dataset."""
8697

@@ -112,6 +123,7 @@ class _ModelConfig(_SchemaConfig):
112123
"""Reader configuration options for the model."""
113124
aliases: dict[FieldName, FieldName] = Field(default_factory=dict)
114125
"""An alias field name mapping."""
126+
linkage_details: Optional[_LinkageConfig] = None
115127

116128

117129
class _RuleStoreConfig(BaseModel):
@@ -330,6 +342,7 @@ def get_contract_metadata(self) -> DataContractMetadata:
330342
validators=validators,
331343
reporting_fields=reporting_fields,
332344
cache_originals=self.contract.cache_originals,
345+
linkage_hierarchy=self.determine_entity_hierarchy()
333346
)
334347

335348
def load_error_message_info(self, uri):
@@ -351,3 +364,22 @@ def get_rule_metadata(self) -> RuleMetadata:
351364
global_variables=self.transformations.parameters, # pylint: disable=E1101
352365
reference_data_config=self.get_reference_data_config(),
353366
)
367+
368+
def determine_entity_hierarchy(self) -> list[HierarchyNode]:
369+
"""Determine the linkage hierarchy using contact config"""
370+
linkage_hierarchy = {name: model_conf.linkage_details for name, model_conf in self.contract.datasets.items()}
371+
top_level_parents = {}
372+
for name, linkage_detail in linkage_hierarchy.items():
373+
if not linkage_detail:
374+
top_level_parents[name] = HierarchyNode(entity_name=name)
375+
continue
376+
for main_entity, details in top_level_parents.items():
377+
if (linkage_detail.parent_entity == main_entity
378+
or linkage_detail.parent_entity in details.get_descendents()):
379+
top_level_parents[main_entity].add_child_node(linkage_detail.parent_entity,
380+
ChildHierarchyNode(entity_name=name,
381+
join_fields=linkage_detail.join_fields))
382+
break
383+
else:
384+
raise EntityNotFoundError(f"Can't find parent entity {linkage_detail.parent_entity} defined to establish hierarchy for {name} - please ensure it is defined above any child entities in the dischema.")
385+
return top_level_parents

‎src/dve/metadata_parser/exc.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
class EntityNotFoundError(KeyError):
55
"""Error for missing entities"""
6-
7-
6+
87
class LocWarning(UserWarning):
98
"""Warning class with optional location parameter"""
109

0 commit comments

Comments
 (0)