cognite-neat 0.76.0__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.
Files changed (52) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/app/api/routers/core.py +1 -1
  3. cognite/neat/app/api/routers/rules.py +1 -1
  4. cognite/neat/graph/extractors/_mock_graph_generator.py +2 -2
  5. cognite/neat/rules/_shared.py +1 -1
  6. cognite/neat/rules/analysis/_information_rules.py +3 -3
  7. cognite/neat/rules/exporters/_base.py +1 -1
  8. cognite/neat/rules/exporters/_rules2dms.py +8 -49
  9. cognite/neat/rules/exporters/_rules2excel.py +9 -3
  10. cognite/neat/rules/exporters/_rules2ontology.py +2 -2
  11. cognite/neat/rules/exporters/_rules2yaml.py +1 -1
  12. cognite/neat/rules/exporters/_validation.py +2 -2
  13. cognite/neat/rules/importers/_base.py +1 -1
  14. cognite/neat/rules/importers/_dms2rules.py +93 -108
  15. cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +1 -1
  16. cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +2 -3
  17. cognite/neat/rules/importers/_owl2rules/_owl2classes.py +1 -1
  18. cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +2 -2
  19. cognite/neat/rules/importers/_owl2rules/_owl2properties.py +1 -1
  20. cognite/neat/rules/importers/_owl2rules/_owl2rules.py +1 -1
  21. cognite/neat/rules/importers/_spreadsheet2rules.py +10 -4
  22. cognite/neat/rules/importers/_yaml2rules.py +3 -3
  23. cognite/neat/rules/issues/base.py +5 -0
  24. cognite/neat/rules/models/__init__.py +27 -0
  25. cognite/neat/rules/models/dms/__init__.py +18 -0
  26. cognite/neat/rules/models/dms/_converter.py +140 -0
  27. cognite/neat/rules/models/dms/_exporter.py +405 -0
  28. cognite/neat/rules/models/dms/_rules.py +379 -0
  29. cognite/neat/rules/models/{rules/_dms_rules_write.py → dms/_rules_input.py} +65 -57
  30. cognite/neat/rules/models/{rules/_dms_schema.py → dms/_schema.py} +10 -4
  31. cognite/neat/rules/models/dms/_serializer.py +126 -0
  32. cognite/neat/rules/models/dms/_validation.py +255 -0
  33. cognite/neat/rules/models/information/__init__.py +3 -0
  34. cognite/neat/rules/models/information/_converter.py +193 -0
  35. cognite/neat/rules/models/{rules/_information_rules.py → information/_rules.py} +35 -202
  36. cognite/neat/workflows/steps/data_contracts.py +1 -1
  37. cognite/neat/workflows/steps/lib/current/rules_exporter.py +9 -3
  38. cognite/neat/workflows/steps/lib/current/rules_importer.py +1 -1
  39. cognite/neat/workflows/steps/lib/current/rules_validator.py +1 -2
  40. {cognite_neat-0.76.0.dist-info → cognite_neat-0.76.2.dist-info}/METADATA +1 -1
  41. {cognite_neat-0.76.0.dist-info → cognite_neat-0.76.2.dist-info}/RECORD +50 -43
  42. cognite/neat/rules/models/rules/__init__.py +0 -14
  43. cognite/neat/rules/models/rules/_dms_architect_rules.py +0 -1255
  44. /cognite/neat/rules/models/{rules/_base.py → _base.py} +0 -0
  45. /cognite/neat/rules/models/{rdfpath.py → _rdfpath.py} +0 -0
  46. /cognite/neat/rules/models/{rules/_types → _types}/__init__.py +0 -0
  47. /cognite/neat/rules/models/{rules/_types → _types}/_base.py +0 -0
  48. /cognite/neat/rules/models/{rules/_types → _types}/_field.py +0 -0
  49. /cognite/neat/rules/models/{rules/_domain_rules.py → domain.py} +0 -0
  50. {cognite_neat-0.76.0.dist-info → cognite_neat-0.76.2.dist-info}/LICENSE +0 -0
  51. {cognite_neat-0.76.0.dist-info → cognite_neat-0.76.2.dist-info}/WHEEL +0 -0
  52. {cognite_neat-0.76.0.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