cognite-neat 0.76.1__py3-none-any.whl → 0.76.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cognite/neat/_version.py +1 -1
- cognite/neat/app/api/routers/core.py +1 -1
- cognite/neat/app/api/routers/rules.py +1 -1
- cognite/neat/graph/extractors/_mock_graph_generator.py +2 -2
- cognite/neat/rules/_shared.py +1 -1
- cognite/neat/rules/analysis/_information_rules.py +3 -3
- cognite/neat/rules/exporters/_base.py +1 -1
- cognite/neat/rules/exporters/_rules2dms.py +8 -49
- cognite/neat/rules/exporters/_rules2excel.py +9 -3
- cognite/neat/rules/exporters/_rules2ontology.py +2 -2
- cognite/neat/rules/exporters/_rules2yaml.py +1 -1
- cognite/neat/rules/exporters/_validation.py +2 -2
- cognite/neat/rules/importers/_base.py +1 -1
- cognite/neat/rules/importers/_dms2rules.py +93 -108
- cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +1 -1
- cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +2 -3
- cognite/neat/rules/importers/_owl2rules/_owl2classes.py +1 -1
- cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +2 -2
- cognite/neat/rules/importers/_owl2rules/_owl2properties.py +1 -1
- cognite/neat/rules/importers/_owl2rules/_owl2rules.py +1 -1
- cognite/neat/rules/importers/_spreadsheet2rules.py +10 -4
- cognite/neat/rules/importers/_yaml2rules.py +3 -3
- cognite/neat/rules/issues/base.py +5 -0
- cognite/neat/rules/models/__init__.py +27 -0
- cognite/neat/rules/models/dms/__init__.py +18 -0
- cognite/neat/rules/models/dms/_converter.py +140 -0
- cognite/neat/rules/models/dms/_exporter.py +405 -0
- cognite/neat/rules/models/dms/_rules.py +379 -0
- cognite/neat/rules/models/{rules/_dms_rules_write.py → dms/_rules_input.py} +33 -33
- cognite/neat/rules/models/{rules/_dms_schema.py → dms/_schema.py} +10 -4
- cognite/neat/rules/models/dms/_serializer.py +126 -0
- cognite/neat/rules/models/dms/_validation.py +255 -0
- cognite/neat/rules/models/information/__init__.py +3 -0
- cognite/neat/rules/models/information/_converter.py +193 -0
- cognite/neat/rules/models/{rules/_information_rules.py → information/_rules.py} +35 -202
- cognite/neat/workflows/steps/data_contracts.py +1 -1
- cognite/neat/workflows/steps/lib/current/rules_exporter.py +9 -3
- cognite/neat/workflows/steps/lib/current/rules_importer.py +1 -1
- cognite/neat/workflows/steps/lib/current/rules_validator.py +1 -2
- {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/METADATA +1 -1
- {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/RECORD +50 -43
- cognite/neat/rules/models/rules/__init__.py +0 -14
- cognite/neat/rules/models/rules/_dms_architect_rules.py +0 -1255
- /cognite/neat/rules/models/{rules/_base.py → _base.py} +0 -0
- /cognite/neat/rules/models/{rdfpath.py → _rdfpath.py} +0 -0
- /cognite/neat/rules/models/{rules/_types → _types}/__init__.py +0 -0
- /cognite/neat/rules/models/{rules/_types → _types}/_base.py +0 -0
- /cognite/neat/rules/models/{rules/_types → _types}/_field.py +0 -0
- /cognite/neat/rules/models/{rules/_domain_rules.py → domain.py} +0 -0
- {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/LICENSE +0 -0
- {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/WHEEL +0 -0
- {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sys
|
|
2
|
+
import warnings
|
|
2
3
|
from abc import ABC, abstractmethod
|
|
3
4
|
from collections import UserList
|
|
4
5
|
from collections.abc import Sequence
|
|
@@ -182,6 +183,10 @@ class IssueList(UserList[ValidationIssue]):
|
|
|
182
183
|
[ValueError(issue.message()) for issue in self if isinstance(issue, NeatValidationError)],
|
|
183
184
|
)
|
|
184
185
|
|
|
186
|
+
def trigger_warnings(self) -> None:
|
|
187
|
+
for warning in [issue for issue in self if isinstance(issue, ValidationWarning)]:
|
|
188
|
+
warnings.warn(warning, stacklevel=2)
|
|
189
|
+
|
|
185
190
|
def to_pandas(self) -> pd.DataFrame:
|
|
186
191
|
return pd.DataFrame([issue.dump() for issue in self])
|
|
187
192
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from cognite.neat.rules.models.domain import DomainRules
|
|
2
|
+
from cognite.neat.rules.models.information._rules import InformationRules
|
|
3
|
+
|
|
4
|
+
from ._base import DataModelType, ExtensionCategory, RoleTypes, SchemaCompleteness, SheetEntity, SheetList
|
|
5
|
+
from .dms._rules import DMSRules
|
|
6
|
+
from .dms._schema import DMSSchema
|
|
7
|
+
|
|
8
|
+
RULES_PER_ROLE: dict[RoleTypes, type[DomainRules] | type[InformationRules] | type[DMSRules]] = {
|
|
9
|
+
RoleTypes.domain_expert: DomainRules,
|
|
10
|
+
RoleTypes.information_architect: InformationRules,
|
|
11
|
+
RoleTypes.dms_architect: DMSRules,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"DomainRules",
|
|
17
|
+
"InformationRules",
|
|
18
|
+
"DMSRules",
|
|
19
|
+
"RULES_PER_ROLE",
|
|
20
|
+
"DMSSchema",
|
|
21
|
+
"RoleTypes",
|
|
22
|
+
"SchemaCompleteness",
|
|
23
|
+
"ExtensionCategory",
|
|
24
|
+
"DataModelType",
|
|
25
|
+
"SheetList",
|
|
26
|
+
"SheetEntity",
|
|
27
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from ._rules import DMSContainer, DMSMetadata, DMSProperty, DMSRules, DMSView
|
|
2
|
+
from ._rules_input import DMSContainerInput, DMSMetadataInput, DMSPropertyInput, DMSRulesInput, DMSViewInput
|
|
3
|
+
from ._schema import DMSSchema, PipelineSchema
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"DMSRules",
|
|
7
|
+
"DMSSchema",
|
|
8
|
+
"DMSMetadata",
|
|
9
|
+
"DMSView",
|
|
10
|
+
"DMSProperty",
|
|
11
|
+
"DMSContainer",
|
|
12
|
+
"PipelineSchema",
|
|
13
|
+
"DMSRulesInput",
|
|
14
|
+
"DMSMetadataInput",
|
|
15
|
+
"DMSViewInput",
|
|
16
|
+
"DMSPropertyInput",
|
|
17
|
+
"DMSContainerInput",
|
|
18
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from rdflib import Namespace
|
|
6
|
+
|
|
7
|
+
from cognite.neat.rules import issues
|
|
8
|
+
from cognite.neat.rules.models._base import SheetList
|
|
9
|
+
from cognite.neat.rules.models.data_types import DataType
|
|
10
|
+
from cognite.neat.rules.models.domain import DomainRules
|
|
11
|
+
from cognite.neat.rules.models.entities import (
|
|
12
|
+
ClassEntity,
|
|
13
|
+
ContainerEntity,
|
|
14
|
+
DMSUnknownEntity,
|
|
15
|
+
ParentClassEntity,
|
|
16
|
+
ReferenceEntity,
|
|
17
|
+
UnknownEntity,
|
|
18
|
+
ViewEntity,
|
|
19
|
+
ViewPropertyEntity,
|
|
20
|
+
)
|
|
21
|
+
from cognite.neat.rules.models.information._rules import InformationRules
|
|
22
|
+
|
|
23
|
+
from ._rules import DMSProperty, DMSRules, DMSView
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _DMSRulesConverter:
|
|
27
|
+
def __init__(self, dms: DMSRules):
|
|
28
|
+
self.dms = dms
|
|
29
|
+
|
|
30
|
+
def as_domain_rules(self) -> "DomainRules":
|
|
31
|
+
raise NotImplementedError("DomainRules not implemented yet")
|
|
32
|
+
|
|
33
|
+
def as_information_architect_rules(
|
|
34
|
+
self,
|
|
35
|
+
created: datetime | None = None,
|
|
36
|
+
updated: datetime | None = None,
|
|
37
|
+
name: str | None = None,
|
|
38
|
+
namespace: Namespace | None = None,
|
|
39
|
+
) -> "InformationRules":
|
|
40
|
+
from cognite.neat.rules.models.information._rules import (
|
|
41
|
+
InformationClass,
|
|
42
|
+
InformationMetadata,
|
|
43
|
+
InformationProperty,
|
|
44
|
+
InformationRules,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
dms = self.dms.metadata
|
|
48
|
+
prefix = dms.space
|
|
49
|
+
|
|
50
|
+
metadata = InformationMetadata(
|
|
51
|
+
schema_=dms.schema_,
|
|
52
|
+
prefix=prefix,
|
|
53
|
+
namespace=namespace or Namespace(f"https://purl.orgl/neat/{prefix}/"),
|
|
54
|
+
version=dms.version,
|
|
55
|
+
name=name or dms.name or "Missing name",
|
|
56
|
+
creator=dms.creator,
|
|
57
|
+
created=dms.created or created or datetime.now(),
|
|
58
|
+
updated=dms.updated or updated or datetime.now(),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
classes = [
|
|
62
|
+
InformationClass(
|
|
63
|
+
# we do not want a version in class as we use URI for the class
|
|
64
|
+
class_=ClassEntity(prefix=view.class_.prefix, suffix=view.class_.suffix),
|
|
65
|
+
description=view.description,
|
|
66
|
+
parent=[
|
|
67
|
+
# we do not want a version in class as we use URI for the class
|
|
68
|
+
ParentClassEntity(prefix=implemented_view.prefix, suffix=implemented_view.suffix)
|
|
69
|
+
# We only want parents in the same namespace, parent in a different namespace is a reference
|
|
70
|
+
for implemented_view in view.implements or []
|
|
71
|
+
if implemented_view.prefix == view.class_.prefix
|
|
72
|
+
],
|
|
73
|
+
reference=self._get_class_reference(view),
|
|
74
|
+
)
|
|
75
|
+
for view in self.dms.views
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
properties: list[InformationProperty] = []
|
|
79
|
+
value_type: DataType | ClassEntity | str
|
|
80
|
+
for property_ in self.dms.properties:
|
|
81
|
+
if isinstance(property_.value_type, DataType):
|
|
82
|
+
value_type = property_.value_type
|
|
83
|
+
elif isinstance(property_.value_type, ViewEntity | ViewPropertyEntity):
|
|
84
|
+
value_type = ClassEntity(
|
|
85
|
+
prefix=property_.value_type.prefix,
|
|
86
|
+
suffix=property_.value_type.suffix,
|
|
87
|
+
)
|
|
88
|
+
elif isinstance(property_.value_type, DMSUnknownEntity):
|
|
89
|
+
value_type = UnknownEntity()
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(f"Unsupported value type: {property_.value_type.type_}")
|
|
92
|
+
|
|
93
|
+
properties.append(
|
|
94
|
+
InformationProperty(
|
|
95
|
+
# Removing version
|
|
96
|
+
class_=ClassEntity(suffix=property_.class_.suffix, prefix=property_.class_.prefix),
|
|
97
|
+
property_=property_.view_property,
|
|
98
|
+
value_type=value_type,
|
|
99
|
+
description=property_.description,
|
|
100
|
+
min_count=0 if property_.nullable or property_.nullable is None else 1,
|
|
101
|
+
max_count=float("inf") if property_.is_list or property_.nullable is None else 1,
|
|
102
|
+
reference=self._get_property_reference(property_),
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return InformationRules(
|
|
107
|
+
metadata=metadata,
|
|
108
|
+
properties=SheetList[InformationProperty](data=properties),
|
|
109
|
+
classes=SheetList[InformationClass](data=classes),
|
|
110
|
+
last=self.dms.last.as_information_architect_rules() if self.dms.last else None,
|
|
111
|
+
reference=self.dms.reference.as_information_architect_rules() if self.dms.reference else None,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def _get_class_reference(cls, view: DMSView) -> ReferenceEntity | None:
|
|
116
|
+
parents_other_namespace = [parent for parent in view.implements or [] if parent.prefix != view.class_.prefix]
|
|
117
|
+
if len(parents_other_namespace) == 0:
|
|
118
|
+
return None
|
|
119
|
+
if len(parents_other_namespace) > 1:
|
|
120
|
+
warnings.warn(
|
|
121
|
+
issues.dms.MultipleReferenceWarning(
|
|
122
|
+
view_id=view.view.as_id(), implements=[v.as_id() for v in parents_other_namespace]
|
|
123
|
+
),
|
|
124
|
+
stacklevel=2,
|
|
125
|
+
)
|
|
126
|
+
other_parent = parents_other_namespace[0]
|
|
127
|
+
|
|
128
|
+
return ReferenceEntity(prefix=other_parent.prefix, suffix=other_parent.suffix)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def _get_property_reference(cls, property_: DMSProperty) -> ReferenceEntity | None:
|
|
132
|
+
has_container_other_namespace = property_.container and property_.container.prefix != property_.class_.prefix
|
|
133
|
+
if not has_container_other_namespace:
|
|
134
|
+
return None
|
|
135
|
+
container = cast(ContainerEntity, property_.container)
|
|
136
|
+
return ReferenceEntity(
|
|
137
|
+
prefix=container.prefix,
|
|
138
|
+
suffix=container.suffix,
|
|
139
|
+
property=property_.container_property,
|
|
140
|
+
)
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from cognite.client.data_classes import data_modeling as dm
|
|
6
|
+
from cognite.client.data_classes.data_modeling.containers import BTreeIndex
|
|
7
|
+
from cognite.client.data_classes.data_modeling.views import (
|
|
8
|
+
SingleEdgeConnectionApply,
|
|
9
|
+
SingleReverseDirectRelationApply,
|
|
10
|
+
ViewPropertyApply,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from cognite.neat.rules import issues
|
|
14
|
+
from cognite.neat.rules.models._base import DataModelType
|
|
15
|
+
from cognite.neat.rules.models.data_types import DataType
|
|
16
|
+
from cognite.neat.rules.models.entities import (
|
|
17
|
+
ContainerEntity,
|
|
18
|
+
DMSNodeEntity,
|
|
19
|
+
DMSUnknownEntity,
|
|
20
|
+
ReferenceEntity,
|
|
21
|
+
ViewEntity,
|
|
22
|
+
ViewPropertyEntity,
|
|
23
|
+
)
|
|
24
|
+
from cognite.neat.rules.models.wrapped_entities import DMSFilter, HasDataFilter, NodeTypeFilter
|
|
25
|
+
|
|
26
|
+
from ._rules import DMSMetadata, DMSProperty, DMSRules, DMSView
|
|
27
|
+
from ._schema import DMSSchema, PipelineSchema
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _DMSExporter:
|
|
31
|
+
"""The DMS Exporter is responsible for exporting the DMSRules to a DMSSchema.
|
|
32
|
+
|
|
33
|
+
This kept in this location such that it can be used by the DMSRules to validate the schema.
|
|
34
|
+
(This module cannot have a dependency on the exporter module, as it would create a circular dependency.)
|
|
35
|
+
|
|
36
|
+
Args
|
|
37
|
+
include_pipeline (bool): If True, the pipeline will be included with the schema. Pipeline means the
|
|
38
|
+
raw tables and transformations necessary to populate the data model.
|
|
39
|
+
instance_space (str): The space to use for the instance. Defaults to None,`Rules.metadata.space` will be used
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
rules: DMSRules,
|
|
45
|
+
include_ref: bool = True,
|
|
46
|
+
include_pipeline: bool = False,
|
|
47
|
+
instance_space: str | None = None,
|
|
48
|
+
):
|
|
49
|
+
self.include_ref = include_ref
|
|
50
|
+
self.include_pipeline = include_pipeline
|
|
51
|
+
self.instance_space = instance_space
|
|
52
|
+
self.rules = rules
|
|
53
|
+
self._ref_schema = rules.reference.as_schema() if rules.reference else None
|
|
54
|
+
if self._ref_schema:
|
|
55
|
+
# We skip version as that will always be missing in the reference
|
|
56
|
+
self._ref_views_by_id = {dm.ViewId(view.space, view.external_id): view for view in self._ref_schema.views}
|
|
57
|
+
else:
|
|
58
|
+
self._ref_views_by_id = {}
|
|
59
|
+
|
|
60
|
+
def to_schema(self) -> DMSSchema:
|
|
61
|
+
rules = self.rules
|
|
62
|
+
container_properties_by_id, view_properties_by_id = self._gather_properties()
|
|
63
|
+
containers = self._create_containers(container_properties_by_id)
|
|
64
|
+
|
|
65
|
+
views, node_types = self._create_views_with_node_types(view_properties_by_id)
|
|
66
|
+
|
|
67
|
+
views_not_in_model = {view.view.as_id() for view in rules.views if not view.in_model}
|
|
68
|
+
data_model = rules.metadata.as_data_model()
|
|
69
|
+
data_model.views = sorted(
|
|
70
|
+
[view_id for view_id in views.as_ids() if view_id not in views_not_in_model],
|
|
71
|
+
key=lambda v: v.as_tuple(), # type: ignore[union-attr]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
spaces = self._create_spaces(rules.metadata, containers, views, data_model)
|
|
75
|
+
|
|
76
|
+
output = DMSSchema(
|
|
77
|
+
spaces=spaces,
|
|
78
|
+
data_models=dm.DataModelApplyList([data_model]),
|
|
79
|
+
views=views,
|
|
80
|
+
containers=containers,
|
|
81
|
+
node_types=node_types,
|
|
82
|
+
)
|
|
83
|
+
if self.include_pipeline:
|
|
84
|
+
return PipelineSchema.from_dms(output, self.instance_space)
|
|
85
|
+
|
|
86
|
+
if self._ref_schema:
|
|
87
|
+
output.reference = self._ref_schema
|
|
88
|
+
|
|
89
|
+
return output
|
|
90
|
+
|
|
91
|
+
def _create_spaces(
|
|
92
|
+
self,
|
|
93
|
+
metadata: DMSMetadata,
|
|
94
|
+
containers: dm.ContainerApplyList,
|
|
95
|
+
views: dm.ViewApplyList,
|
|
96
|
+
data_model: dm.DataModelApply,
|
|
97
|
+
) -> dm.SpaceApplyList:
|
|
98
|
+
used_spaces = {container.space for container in containers} | {view.space for view in views}
|
|
99
|
+
if len(used_spaces) == 1:
|
|
100
|
+
# We skip the default space and only use this space for the data model
|
|
101
|
+
data_model.space = used_spaces.pop()
|
|
102
|
+
spaces = dm.SpaceApplyList([dm.SpaceApply(space=data_model.space)])
|
|
103
|
+
else:
|
|
104
|
+
used_spaces.add(metadata.space)
|
|
105
|
+
spaces = dm.SpaceApplyList([dm.SpaceApply(space=space) for space in used_spaces])
|
|
106
|
+
if self.instance_space and self.instance_space not in {space.space for space in spaces}:
|
|
107
|
+
spaces.append(dm.SpaceApply(space=self.instance_space, name=self.instance_space))
|
|
108
|
+
return spaces
|
|
109
|
+
|
|
110
|
+
def _create_views_with_node_types(
|
|
111
|
+
self,
|
|
112
|
+
view_properties_by_id: dict[dm.ViewId, list[DMSProperty]],
|
|
113
|
+
) -> tuple[dm.ViewApplyList, dm.NodeApplyList]:
|
|
114
|
+
views = dm.ViewApplyList([dms_view.as_view() for dms_view in self.rules.views])
|
|
115
|
+
dms_view_by_id = {dms_view.view.as_id(): dms_view for dms_view in self.rules.views}
|
|
116
|
+
|
|
117
|
+
for view in views:
|
|
118
|
+
view_id = view.as_id()
|
|
119
|
+
view.properties = {}
|
|
120
|
+
if not (view_properties := view_properties_by_id.get(view_id)):
|
|
121
|
+
continue
|
|
122
|
+
for prop in view_properties:
|
|
123
|
+
view_property = self._create_view_property(prop, view_properties_by_id)
|
|
124
|
+
if view_property is not None:
|
|
125
|
+
view.properties[prop.view_property] = view_property
|
|
126
|
+
|
|
127
|
+
data_model_type = self.rules.metadata.data_model_type
|
|
128
|
+
unique_node_types: set[dm.NodeId] = set()
|
|
129
|
+
parent_views = {parent for view in views for parent in view.implements or []}
|
|
130
|
+
for view in views:
|
|
131
|
+
dms_view = dms_view_by_id.get(view.as_id())
|
|
132
|
+
dms_properties = view_properties_by_id.get(view.as_id(), [])
|
|
133
|
+
view_filter = self._create_view_filter(view, dms_view, data_model_type, dms_properties)
|
|
134
|
+
|
|
135
|
+
view.filter = view_filter.as_dms_filter()
|
|
136
|
+
|
|
137
|
+
if isinstance(view_filter, NodeTypeFilter):
|
|
138
|
+
unique_node_types.update(view_filter.nodes)
|
|
139
|
+
if view.as_id() in parent_views:
|
|
140
|
+
warnings.warn(issues.dms.NodeTypeFilterOnParentViewWarning(view.as_id()), stacklevel=2)
|
|
141
|
+
elif isinstance(view_filter, HasDataFilter) and data_model_type == DataModelType.solution:
|
|
142
|
+
if dms_view and isinstance(dms_view.reference, ReferenceEntity):
|
|
143
|
+
references = {dms_view.reference.as_view_id()}
|
|
144
|
+
elif any(True for prop in dms_properties if isinstance(prop.reference, ReferenceEntity)):
|
|
145
|
+
references = {
|
|
146
|
+
prop.reference.as_view_id()
|
|
147
|
+
for prop in dms_properties
|
|
148
|
+
if isinstance(prop.reference, ReferenceEntity)
|
|
149
|
+
}
|
|
150
|
+
else:
|
|
151
|
+
continue
|
|
152
|
+
warnings.warn(
|
|
153
|
+
issues.dms.HasDataFilterOnViewWithReferencesWarning(view.as_id(), list(references)), stacklevel=2
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return views, dm.NodeApplyList(
|
|
157
|
+
[dm.NodeApply(space=node.space, external_id=node.external_id) for node in unique_node_types]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def _create_edge_type_from_prop(cls, prop: DMSProperty) -> dm.DirectRelationReference:
|
|
162
|
+
if isinstance(prop.reference, ReferenceEntity):
|
|
163
|
+
ref_view_prop = prop.reference.as_view_property_id()
|
|
164
|
+
return cls._create_edge_type_from_view_id(cast(dm.ViewId, ref_view_prop.source), ref_view_prop.property)
|
|
165
|
+
else:
|
|
166
|
+
return cls._create_edge_type_from_view_id(prop.view.as_id(), prop.view_property)
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _create_edge_type_from_view_id(view_id: dm.ViewId, property_: str) -> dm.DirectRelationReference:
|
|
170
|
+
return dm.DirectRelationReference(
|
|
171
|
+
space=view_id.space,
|
|
172
|
+
# This is the same convention as used when converting GraphQL to DMS
|
|
173
|
+
external_id=f"{view_id.external_id}.{property_}",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _create_containers(
|
|
177
|
+
self,
|
|
178
|
+
container_properties_by_id: dict[dm.ContainerId, list[DMSProperty]],
|
|
179
|
+
) -> dm.ContainerApplyList:
|
|
180
|
+
containers = dm.ContainerApplyList(
|
|
181
|
+
[dms_container.as_container() for dms_container in self.rules.containers or []]
|
|
182
|
+
)
|
|
183
|
+
container_to_drop = set()
|
|
184
|
+
for container in containers:
|
|
185
|
+
container_id = container.as_id()
|
|
186
|
+
if not (container_properties := container_properties_by_id.get(container_id)):
|
|
187
|
+
warnings.warn(issues.dms.EmptyContainerWarning(container_id=container_id), stacklevel=2)
|
|
188
|
+
container_to_drop.add(container_id)
|
|
189
|
+
continue
|
|
190
|
+
for prop in container_properties:
|
|
191
|
+
if prop.container_property is None:
|
|
192
|
+
continue
|
|
193
|
+
if isinstance(prop.value_type, DataType):
|
|
194
|
+
type_cls = prop.value_type.dms
|
|
195
|
+
else:
|
|
196
|
+
type_cls = dm.DirectRelation
|
|
197
|
+
|
|
198
|
+
type_ = type_cls(is_list=prop.is_list or False)
|
|
199
|
+
container.properties[prop.container_property] = dm.ContainerProperty(
|
|
200
|
+
type=type_,
|
|
201
|
+
nullable=prop.nullable if prop.nullable is not None else True,
|
|
202
|
+
default_value=prop.default,
|
|
203
|
+
name=prop.name,
|
|
204
|
+
description=prop.description,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
uniqueness_properties: dict[str, set[str]] = defaultdict(set)
|
|
208
|
+
for prop in container_properties:
|
|
209
|
+
if prop.container_property is not None:
|
|
210
|
+
for constraint in prop.constraint or []:
|
|
211
|
+
uniqueness_properties[constraint].add(prop.container_property)
|
|
212
|
+
for constraint_name, properties in uniqueness_properties.items():
|
|
213
|
+
container.constraints = container.constraints or {}
|
|
214
|
+
container.constraints[constraint_name] = dm.UniquenessConstraint(properties=list(properties))
|
|
215
|
+
|
|
216
|
+
index_properties: dict[str, set[str]] = defaultdict(set)
|
|
217
|
+
for prop in container_properties:
|
|
218
|
+
if prop.container_property is not None:
|
|
219
|
+
for index in prop.index or []:
|
|
220
|
+
index_properties[index].add(prop.container_property)
|
|
221
|
+
for index_name, properties in index_properties.items():
|
|
222
|
+
container.indexes = container.indexes or {}
|
|
223
|
+
container.indexes[index_name] = BTreeIndex(properties=list(properties))
|
|
224
|
+
|
|
225
|
+
# We might drop containers we convert direct relations of list into multi-edge connections
|
|
226
|
+
# which do not have a container.
|
|
227
|
+
for container in containers:
|
|
228
|
+
if container.constraints:
|
|
229
|
+
container.constraints = {
|
|
230
|
+
name: const
|
|
231
|
+
for name, const in container.constraints.items()
|
|
232
|
+
if not (isinstance(const, dm.RequiresConstraint) and const.require in container_to_drop)
|
|
233
|
+
}
|
|
234
|
+
return dm.ContainerApplyList(
|
|
235
|
+
[container for container in containers if container.as_id() not in container_to_drop]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def _gather_properties(self) -> tuple[dict[dm.ContainerId, list[DMSProperty]], dict[dm.ViewId, list[DMSProperty]]]:
|
|
239
|
+
container_properties_by_id: dict[dm.ContainerId, list[DMSProperty]] = defaultdict(list)
|
|
240
|
+
view_properties_by_id: dict[dm.ViewId, list[DMSProperty]] = defaultdict(list)
|
|
241
|
+
for prop in self.rules.properties:
|
|
242
|
+
view_id = prop.view.as_id()
|
|
243
|
+
view_properties_by_id[view_id].append(prop)
|
|
244
|
+
|
|
245
|
+
if prop.container and prop.container_property:
|
|
246
|
+
container_id = prop.container.as_id()
|
|
247
|
+
container_properties_by_id[container_id].append(prop)
|
|
248
|
+
|
|
249
|
+
return container_properties_by_id, view_properties_by_id
|
|
250
|
+
|
|
251
|
+
def _create_view_filter(
|
|
252
|
+
self,
|
|
253
|
+
view: dm.ViewApply,
|
|
254
|
+
dms_view: DMSView | None,
|
|
255
|
+
data_model_type: DataModelType,
|
|
256
|
+
dms_properties: list[DMSProperty],
|
|
257
|
+
) -> DMSFilter:
|
|
258
|
+
selected_filter_name = (dms_view and dms_view.filter_ and dms_view.filter_.name) or ""
|
|
259
|
+
if dms_view and dms_view.filter_ and not dms_view.filter_.is_empty:
|
|
260
|
+
# Has Explicit Filter, use it
|
|
261
|
+
return dms_view.filter_
|
|
262
|
+
|
|
263
|
+
if data_model_type == DataModelType.solution and selected_filter_name in [NodeTypeFilter.name, ""]:
|
|
264
|
+
if (
|
|
265
|
+
dms_view
|
|
266
|
+
and isinstance(dms_view.reference, ReferenceEntity)
|
|
267
|
+
and not dms_properties
|
|
268
|
+
and (ref_view := self._ref_views_by_id.get(dms_view.reference.as_view_id()))
|
|
269
|
+
and ref_view.filter
|
|
270
|
+
):
|
|
271
|
+
# No new properties, only reference, reuse the reference filter
|
|
272
|
+
return DMSFilter.from_dms_filter(ref_view.filter)
|
|
273
|
+
else:
|
|
274
|
+
referenced_node_ids = {
|
|
275
|
+
prop.reference.as_node_entity()
|
|
276
|
+
for prop in dms_properties
|
|
277
|
+
if isinstance(prop.reference, ReferenceEntity)
|
|
278
|
+
}
|
|
279
|
+
if dms_view and isinstance(dms_view.reference, ReferenceEntity):
|
|
280
|
+
referenced_node_ids.add(dms_view.reference.as_node_entity())
|
|
281
|
+
if referenced_node_ids:
|
|
282
|
+
return NodeTypeFilter(inner=list(referenced_node_ids))
|
|
283
|
+
|
|
284
|
+
# Enterprise Model or (Solution + HasData)
|
|
285
|
+
ref_containers = view.referenced_containers()
|
|
286
|
+
if not ref_containers or selected_filter_name == HasDataFilter.name:
|
|
287
|
+
# Child filter without container properties
|
|
288
|
+
if selected_filter_name == HasDataFilter.name:
|
|
289
|
+
warnings.warn(issues.dms.HasDataFilterOnNoPropertiesViewWarning(view.as_id()), stacklevel=2)
|
|
290
|
+
return NodeTypeFilter(inner=[DMSNodeEntity(space=view.space, externalId=view.external_id)])
|
|
291
|
+
else:
|
|
292
|
+
# HasData or not provided (this is the default)
|
|
293
|
+
return HasDataFilter(inner=[ContainerEntity.from_id(id_) for id_ in ref_containers])
|
|
294
|
+
|
|
295
|
+
def _create_view_property(
|
|
296
|
+
self, prop: DMSProperty, view_properties_by_id: dict[dm.ViewId, list[DMSProperty]]
|
|
297
|
+
) -> ViewPropertyApply | None:
|
|
298
|
+
if prop.container and prop.container_property:
|
|
299
|
+
container_prop_identifier = prop.container_property
|
|
300
|
+
extra_args: dict[str, Any] = {}
|
|
301
|
+
if prop.connection == "direct":
|
|
302
|
+
if isinstance(prop.value_type, ViewEntity):
|
|
303
|
+
extra_args["source"] = prop.value_type.as_id()
|
|
304
|
+
elif isinstance(prop.value_type, DMSUnknownEntity):
|
|
305
|
+
extra_args["source"] = None
|
|
306
|
+
else:
|
|
307
|
+
# Should have been validated.
|
|
308
|
+
raise ValueError(
|
|
309
|
+
"If this error occurs it is a bug in NEAT, please report"
|
|
310
|
+
f"Debug Info, Invalid valueType direct: {prop.model_dump_json()}"
|
|
311
|
+
)
|
|
312
|
+
elif prop.connection is not None:
|
|
313
|
+
# Should have been validated.
|
|
314
|
+
raise ValueError(
|
|
315
|
+
"If this error occurs it is a bug in NEAT, please report"
|
|
316
|
+
f"Debug Info, Invalid connection: {prop.model_dump_json()}"
|
|
317
|
+
)
|
|
318
|
+
return dm.MappedPropertyApply(
|
|
319
|
+
container=prop.container.as_id(),
|
|
320
|
+
container_property_identifier=container_prop_identifier,
|
|
321
|
+
name=prop.name,
|
|
322
|
+
description=prop.description,
|
|
323
|
+
**extra_args,
|
|
324
|
+
)
|
|
325
|
+
elif prop.connection == "edge":
|
|
326
|
+
if isinstance(prop.value_type, ViewEntity):
|
|
327
|
+
source_view_id = prop.value_type.as_id()
|
|
328
|
+
else:
|
|
329
|
+
# Should have been validated.
|
|
330
|
+
raise ValueError(
|
|
331
|
+
"If this error occurs it is a bug in NEAT, please report"
|
|
332
|
+
f"Debug Info, Invalid valueType edge: {prop.model_dump_json()}"
|
|
333
|
+
)
|
|
334
|
+
edge_cls: type[dm.EdgeConnectionApply] = dm.MultiEdgeConnectionApply
|
|
335
|
+
# If is_list is not set, we default to a MultiEdgeConnection
|
|
336
|
+
if prop.is_list is False:
|
|
337
|
+
edge_cls = SingleEdgeConnectionApply
|
|
338
|
+
|
|
339
|
+
return edge_cls(
|
|
340
|
+
type=self._create_edge_type_from_prop(prop),
|
|
341
|
+
source=source_view_id,
|
|
342
|
+
direction="outwards",
|
|
343
|
+
name=prop.name,
|
|
344
|
+
description=prop.description,
|
|
345
|
+
)
|
|
346
|
+
elif prop.connection == "reverse":
|
|
347
|
+
reverse_prop_id: str | None = None
|
|
348
|
+
if isinstance(prop.value_type, ViewPropertyEntity):
|
|
349
|
+
source_view_id = prop.value_type.as_view_id()
|
|
350
|
+
reverse_prop_id = prop.value_type.property_
|
|
351
|
+
elif isinstance(prop.value_type, ViewEntity):
|
|
352
|
+
source_view_id = prop.value_type.as_id()
|
|
353
|
+
else:
|
|
354
|
+
# Should have been validated.
|
|
355
|
+
raise ValueError(
|
|
356
|
+
"If this error occurs it is a bug in NEAT, please report"
|
|
357
|
+
f"Debug Info, Invalid valueType reverse connection: {prop.model_dump_json()}"
|
|
358
|
+
)
|
|
359
|
+
reverse_prop: DMSProperty | None = None
|
|
360
|
+
if reverse_prop_id is not None:
|
|
361
|
+
reverse_prop = next(
|
|
362
|
+
(
|
|
363
|
+
prop
|
|
364
|
+
for prop in view_properties_by_id.get(source_view_id, [])
|
|
365
|
+
if prop.property_ == reverse_prop_id
|
|
366
|
+
),
|
|
367
|
+
None,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
if reverse_prop is None:
|
|
371
|
+
warnings.warn(
|
|
372
|
+
issues.dms.ReverseRelationMissingOtherSideWarning(source_view_id, prop.view_property),
|
|
373
|
+
stacklevel=2,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
if reverse_prop is None or reverse_prop.connection == "edge":
|
|
377
|
+
inwards_edge_cls = (
|
|
378
|
+
dm.MultiEdgeConnectionApply if prop.is_list in [True, None] else SingleEdgeConnectionApply
|
|
379
|
+
)
|
|
380
|
+
return inwards_edge_cls(
|
|
381
|
+
type=self._create_edge_type_from_prop(reverse_prop or prop),
|
|
382
|
+
source=source_view_id,
|
|
383
|
+
name=prop.name,
|
|
384
|
+
description=prop.description,
|
|
385
|
+
direction="inwards",
|
|
386
|
+
)
|
|
387
|
+
elif reverse_prop_id and reverse_prop and reverse_prop.connection == "direct":
|
|
388
|
+
reverse_direct_cls = (
|
|
389
|
+
dm.MultiReverseDirectRelationApply if prop.is_list is True else SingleReverseDirectRelationApply
|
|
390
|
+
)
|
|
391
|
+
return reverse_direct_cls(
|
|
392
|
+
source=source_view_id,
|
|
393
|
+
through=dm.PropertyId(source=source_view_id, property=reverse_prop_id),
|
|
394
|
+
name=prop.name,
|
|
395
|
+
description=prop.description,
|
|
396
|
+
)
|
|
397
|
+
else:
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
elif prop.view and prop.view_property and prop.connection:
|
|
401
|
+
warnings.warn(
|
|
402
|
+
issues.dms.UnsupportedConnectionWarning(prop.view.as_id(), prop.view_property, prop.connection or ""),
|
|
403
|
+
stacklevel=2,
|
|
404
|
+
)
|
|
405
|
+
return None
|