cognite-neat 0.76.1__py3-none-any.whl → 0.76.3__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.

Potentially problematic release.


This version of cognite-neat might be problematic. Click here for more details.

Files changed (53) 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 +71 -40
  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 +87 -62
  22. cognite/neat/rules/importers/_yaml2rules.py +3 -3
  23. cognite/neat/rules/issues/base.py +5 -0
  24. cognite/neat/rules/issues/dms.py +65 -0
  25. cognite/neat/rules/models/__init__.py +27 -0
  26. cognite/neat/rules/models/dms/__init__.py +18 -0
  27. cognite/neat/rules/models/dms/_converter.py +140 -0
  28. cognite/neat/rules/models/dms/_exporter.py +405 -0
  29. cognite/neat/rules/models/dms/_rules.py +379 -0
  30. cognite/neat/rules/models/{rules/_dms_rules_write.py → dms/_rules_input.py} +42 -33
  31. cognite/neat/rules/models/{rules/_dms_schema.py → dms/_schema.py} +36 -4
  32. cognite/neat/rules/models/dms/_serializer.py +126 -0
  33. cognite/neat/rules/models/dms/_validation.py +288 -0
  34. cognite/neat/rules/models/{rules/_domain_rules.py → domain.py} +1 -0
  35. cognite/neat/rules/models/information/__init__.py +3 -0
  36. cognite/neat/rules/models/information/_converter.py +195 -0
  37. cognite/neat/rules/models/{rules/_information_rules.py → information/_rules.py} +35 -202
  38. cognite/neat/workflows/steps/data_contracts.py +1 -1
  39. cognite/neat/workflows/steps/lib/current/rules_exporter.py +10 -3
  40. cognite/neat/workflows/steps/lib/current/rules_importer.py +1 -1
  41. cognite/neat/workflows/steps/lib/current/rules_validator.py +1 -2
  42. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.3.dist-info}/METADATA +1 -1
  43. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.3.dist-info}/RECORD +51 -44
  44. cognite/neat/rules/models/rules/__init__.py +0 -14
  45. cognite/neat/rules/models/rules/_dms_architect_rules.py +0 -1255
  46. /cognite/neat/rules/models/{rules/_base.py → _base.py} +0 -0
  47. /cognite/neat/rules/models/{rdfpath.py → _rdfpath.py} +0 -0
  48. /cognite/neat/rules/models/{rules/_types → _types}/__init__.py +0 -0
  49. /cognite/neat/rules/models/{rules/_types → _types}/_base.py +0 -0
  50. /cognite/neat/rules/models/{rules/_types → _types}/_field.py +0 -0
  51. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.3.dist-info}/LICENSE +0 -0
  52. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.3.dist-info}/WHEEL +0 -0
  53. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,126 @@
1
+ from typing import Any, ClassVar, cast
2
+
3
+ from pydantic_core.core_schema import SerializationInfo
4
+
5
+ from cognite.neat.rules.models import DMSRules
6
+ from cognite.neat.rules.models.dms import DMSContainer, DMSProperty, DMSView
7
+
8
+
9
+ class _DMSRulesSerializer:
10
+ # These are the fields that need to be cleaned from the default space and version
11
+ PROPERTIES_FIELDS: ClassVar[list[str]] = ["class_", "view", "value_type", "container"]
12
+ VIEWS_FIELDS: ClassVar[list[str]] = ["class_", "view", "implements"]
13
+ CONTAINERS_FIELDS: ClassVar[list[str]] = ["class_", "container"]
14
+
15
+ def __init__(self, info: SerializationInfo, default_space: str, default_version: str) -> None:
16
+ self.default_space = f"{default_space}:"
17
+ self.default_version = f"version={default_version}"
18
+ self.default_version_wrapped = f"({self.default_version})"
19
+
20
+ self.properties_fields = self.PROPERTIES_FIELDS
21
+ self.views_fields = self.VIEWS_FIELDS
22
+ self.containers_fields = self.CONTAINERS_FIELDS
23
+ self.prop_name = "properties"
24
+ self.view_name = "views"
25
+ self.container_name = "containers"
26
+ self.metadata_name = "metadata"
27
+ self.prop_view = "view"
28
+ self.prop_view_property = "view_property"
29
+ self.prop_value_type = "value_type"
30
+ self.view_view = "view"
31
+ self.view_implements = "implements"
32
+ self.container_container = "container"
33
+ self.container_constraint = "constraint"
34
+
35
+ if info.by_alias:
36
+ self.properties_fields = [
37
+ DMSProperty.model_fields[field].alias or field for field in self.properties_fields
38
+ ]
39
+ self.views_fields = [DMSView.model_fields[field].alias or field for field in self.views_fields]
40
+ self.container_fields = [
41
+ DMSContainer.model_fields[field].alias or field for field in self.containers_fields
42
+ ]
43
+ self.prop_view = DMSProperty.model_fields[self.prop_view].alias or self.prop_view
44
+ self.prop_view_property = DMSProperty.model_fields[self.prop_view_property].alias or self.prop_view_property
45
+ self.prop_value_type = DMSProperty.model_fields[self.prop_value_type].alias or self.prop_value_type
46
+ self.view_view = DMSView.model_fields[self.view_view].alias or self.view_view
47
+ self.view_implements = DMSView.model_fields[self.view_implements].alias or self.view_implements
48
+ self.container_container = (
49
+ DMSContainer.model_fields[self.container_container].alias or self.container_container
50
+ )
51
+ self.container_constraint = (
52
+ DMSContainer.model_fields[self.container_constraint].alias or self.container_constraint
53
+ )
54
+ self.prop_name = DMSRules.model_fields[self.prop_name].alias or self.prop_name
55
+ self.view_name = DMSRules.model_fields[self.view_name].alias or self.view_name
56
+ self.container_name = DMSRules.model_fields[self.container_name].alias or self.container_name
57
+ self.metadata_name = DMSRules.model_fields[self.metadata_name].alias or self.metadata_name
58
+
59
+ if isinstance(info.exclude, dict):
60
+ # Just for happy mypy
61
+ exclude = cast(dict, info.exclude)
62
+ self.exclude_properties = exclude.get("properties", {}).get("__all__", set())
63
+ self.exclude_views = exclude.get("views", {}).get("__all__", set()) or set()
64
+ self.exclude_containers = exclude.get("containers", {}).get("__all__", set()) or set()
65
+ self.metadata_exclude = exclude.get("metadata", set()) or set()
66
+ self.exclude_top = {k for k, v in exclude.items() if not v}
67
+ else:
68
+ self.exclude_top = set(info.exclude or {})
69
+ self.exclude_properties = set()
70
+ self.exclude_views = set()
71
+ self.exclude_containers = set()
72
+ self.metadata_exclude = set()
73
+
74
+ def clean(self, dumped: dict[str, Any]) -> dict[str, Any]:
75
+ # Sorting to get a deterministic order
76
+ dumped[self.prop_name] = sorted(
77
+ dumped[self.prop_name]["data"], key=lambda p: (p[self.prop_view], p[self.prop_view_property])
78
+ )
79
+ dumped[self.view_name] = sorted(dumped[self.view_name]["data"], key=lambda v: v[self.view_view])
80
+ if container_data := dumped.get(self.container_name):
81
+ dumped[self.container_name] = sorted(container_data["data"], key=lambda c: c[self.container_container])
82
+ else:
83
+ dumped.pop(self.container_name, None)
84
+
85
+ for prop in dumped[self.prop_name]:
86
+ for field_name in self.properties_fields:
87
+ if value := prop.get(field_name):
88
+ prop[field_name] = value.removeprefix(self.default_space).removesuffix(self.default_version_wrapped)
89
+ # Value type can have a property as well
90
+ prop[self.prop_value_type] = prop[self.prop_value_type].replace(self.default_version, "")
91
+ if self.exclude_properties:
92
+ for field in self.exclude_properties:
93
+ prop.pop(field, None)
94
+
95
+ for view in dumped[self.view_name]:
96
+ for field_name in self.views_fields:
97
+ if value := view.get(field_name):
98
+ view[field_name] = value.removeprefix(self.default_space).removesuffix(self.default_version_wrapped)
99
+ if value := view.get(self.view_implements):
100
+ view[self.view_implements] = ",".join(
101
+ parent.strip().removeprefix(self.default_space).removesuffix(self.default_version_wrapped)
102
+ for parent in value.split(",")
103
+ )
104
+ if self.exclude_views:
105
+ for field in self.exclude_views:
106
+ view.pop(field, None)
107
+
108
+ for container in dumped.get(self.container_name, []):
109
+ for field_name in self.containers_fields:
110
+ if value := container.get(field_name):
111
+ container[field_name] = value.removeprefix(self.default_space)
112
+
113
+ if value := container.get(self.container_constraint):
114
+ container[self.container_constraint] = ",".join(
115
+ constraint.strip().removeprefix(self.default_space) for constraint in value.split(",")
116
+ )
117
+ if self.exclude_containers:
118
+ for field in self.exclude_containers:
119
+ container.pop(field, None)
120
+
121
+ if self.metadata_exclude:
122
+ for field in self.metadata_exclude:
123
+ dumped[self.metadata_name].pop(field, None)
124
+ for field in self.exclude_top:
125
+ dumped.pop(field, None)
126
+ return dumped
@@ -0,0 +1,288 @@
1
+ from collections import defaultdict
2
+ from typing import Any
3
+
4
+ from cognite.client import data_modeling as dm
5
+
6
+ from cognite.neat.rules import issues
7
+ from cognite.neat.rules.issues import IssueList
8
+ from cognite.neat.rules.models._base import ExtensionCategory, SchemaCompleteness
9
+ from cognite.neat.rules.models.data_types import DataType
10
+ from cognite.neat.rules.models.entities import ContainerEntity
11
+
12
+ from ._rules import DMSProperty, DMSRules
13
+
14
+
15
+ class DMSPostValidation:
16
+ """This class does all the validation of the DMS rules that have dependencies between
17
+ components."""
18
+
19
+ def __init__(self, rules: DMSRules):
20
+ self.rules = rules
21
+ self.metadata = rules.metadata
22
+ self.properties = rules.properties
23
+ self.containers = rules.containers
24
+ self.views = rules.views
25
+ self.issue_list = IssueList()
26
+
27
+ def validate(self) -> IssueList:
28
+ self._consistent_container_properties()
29
+ self._referenced_views_and_containers_are_existing()
30
+ self._validate_extension()
31
+ self._validate_schema()
32
+ self._validate_performance()
33
+ return self.issue_list
34
+
35
+ def _consistent_container_properties(self) -> None:
36
+ container_properties_by_id: dict[tuple[ContainerEntity, str], list[tuple[int, DMSProperty]]] = defaultdict(list)
37
+ for prop_no, prop in enumerate(self.properties):
38
+ if prop.container and prop.container_property:
39
+ container_properties_by_id[(prop.container, prop.container_property)].append((prop_no, prop))
40
+
41
+ errors: list[issues.spreadsheet.InconsistentContainerDefinitionError] = []
42
+ for (container, prop_name), properties in container_properties_by_id.items():
43
+ if len(properties) == 1:
44
+ continue
45
+ container_id = container.as_id()
46
+ row_numbers = {prop_no for prop_no, _ in properties}
47
+ value_types = {prop.value_type for _, prop in properties if prop.value_type}
48
+ if len(value_types) > 1:
49
+ errors.append(
50
+ issues.spreadsheet.MultiValueTypeError(
51
+ container_id,
52
+ prop_name,
53
+ row_numbers,
54
+ {v.dms._type if isinstance(v, DataType) else str(v) for v in value_types},
55
+ )
56
+ )
57
+ list_definitions = {prop.is_list for _, prop in properties if prop.is_list is not None}
58
+ if len(list_definitions) > 1:
59
+ errors.append(
60
+ issues.spreadsheet.MultiValueIsListError(container_id, prop_name, row_numbers, list_definitions)
61
+ )
62
+ nullable_definitions = {prop.nullable for _, prop in properties if prop.nullable is not None}
63
+ if len(nullable_definitions) > 1:
64
+ errors.append(
65
+ issues.spreadsheet.MultiNullableError(container_id, prop_name, row_numbers, nullable_definitions)
66
+ )
67
+ default_definitions = {prop.default for _, prop in properties if prop.default is not None}
68
+ if len(default_definitions) > 1:
69
+ errors.append(
70
+ issues.spreadsheet.MultiDefaultError(
71
+ container_id, prop_name, row_numbers, list(default_definitions)
72
+ )
73
+ )
74
+ index_definitions = {",".join(prop.index) for _, prop in properties if prop.index is not None}
75
+ if len(index_definitions) > 1:
76
+ errors.append(
77
+ issues.spreadsheet.MultiIndexError(container_id, prop_name, row_numbers, index_definitions)
78
+ )
79
+ constraint_definitions = {
80
+ ",".join(prop.constraint) for _, prop in properties if prop.constraint is not None
81
+ }
82
+ if len(constraint_definitions) > 1:
83
+ errors.append(
84
+ issues.spreadsheet.MultiUniqueConstraintError(
85
+ container_id, prop_name, row_numbers, constraint_definitions
86
+ )
87
+ )
88
+
89
+ # This sets the container definition for all the properties where it is not defined.
90
+ # This allows the user to define the container only once.
91
+ value_type = next(iter(value_types))
92
+ list_definition = next(iter(list_definitions)) if list_definitions else None
93
+ nullable_definition = next(iter(nullable_definitions)) if nullable_definitions else None
94
+ default_definition = next(iter(default_definitions)) if default_definitions else None
95
+ index_definition = next(iter(index_definitions)).split(",") if index_definitions else None
96
+ constraint_definition = next(iter(constraint_definitions)).split(",") if constraint_definitions else None
97
+ for _, prop in properties:
98
+ prop.value_type = value_type
99
+ prop.is_list = prop.is_list or list_definition
100
+ prop.nullable = prop.nullable or nullable_definition
101
+ prop.default = prop.default or default_definition
102
+ prop.index = prop.index or index_definition
103
+ prop.constraint = prop.constraint or constraint_definition
104
+ self.issue_list.extend(errors)
105
+
106
+ def _referenced_views_and_containers_are_existing(self) -> None:
107
+ # There two checks are done in the same method to raise all the errors at once.
108
+ defined_views = {view.view.as_id() for view in self.views}
109
+
110
+ errors: list[issues.NeatValidationError] = []
111
+ for prop_no, prop in enumerate(self.properties):
112
+ if prop.view and (view_id := prop.view.as_id()) not in defined_views:
113
+ errors.append(
114
+ issues.spreadsheet.NonExistingViewError(
115
+ column="View",
116
+ row=prop_no,
117
+ type="value_error.missing",
118
+ view_id=view_id,
119
+ msg="",
120
+ input=None,
121
+ url=None,
122
+ )
123
+ )
124
+ if self.metadata.schema_ is SchemaCompleteness.complete:
125
+ defined_containers = {container.container.as_id() for container in self.containers or []}
126
+ for prop_no, prop in enumerate(self.properties):
127
+ if prop.container and (container_id := prop.container.as_id()) not in defined_containers:
128
+ errors.append(
129
+ issues.spreadsheet.NonExistingContainerError(
130
+ column="Container",
131
+ row=prop_no,
132
+ type="value_error.missing",
133
+ container_id=container_id,
134
+ msg="",
135
+ input=None,
136
+ url=None,
137
+ )
138
+ )
139
+ for _container_no, container in enumerate(self.containers or []):
140
+ for constraint_no, constraint in enumerate(container.constraint or []):
141
+ if constraint.as_id() not in defined_containers:
142
+ errors.append(
143
+ issues.spreadsheet.NonExistingContainerError(
144
+ column="Constraint",
145
+ row=constraint_no,
146
+ type="value_error.missing",
147
+ container_id=constraint.as_id(),
148
+ msg="",
149
+ input=None,
150
+ url=None,
151
+ )
152
+ )
153
+ self.issue_list.extend(errors)
154
+
155
+ def _validate_extension(self) -> None:
156
+ if self.metadata.schema_ is not SchemaCompleteness.extended:
157
+ return None
158
+ if not self.rules.reference:
159
+ raise ValueError("The schema is set to 'extended', but no reference rules are provided to validate against")
160
+ is_solution = self.metadata.space != self.rules.reference.metadata.space
161
+ if is_solution:
162
+ return None
163
+ if self.metadata.extension is ExtensionCategory.rebuild:
164
+ # Everything is allowed
165
+ return None
166
+ # Is an extension of an existing model.
167
+ user_schema = self.rules.as_schema(include_ref=False)
168
+ ref_schema = self.rules.reference.as_schema()
169
+ new_containers = {container.as_id(): container for container in user_schema.containers}
170
+ existing_containers = {container.as_id(): container for container in ref_schema.containers}
171
+
172
+ for container_id, container in new_containers.items():
173
+ existing_container = existing_containers.get(container_id)
174
+ if not existing_container or existing_container == container:
175
+ # No problem
176
+ continue
177
+ new_dumped = container.dump()
178
+ existing_dumped = existing_container.dump()
179
+ changed_attributes, changed_properties = self._changed_attributes_and_properties(
180
+ new_dumped, existing_dumped
181
+ )
182
+ self.issue_list.append(
183
+ issues.dms.ChangingContainerError(
184
+ container_id=container_id,
185
+ changed_properties=changed_properties or None,
186
+ changed_attributes=changed_attributes or None,
187
+ )
188
+ )
189
+
190
+ if self.metadata.extension is ExtensionCategory.reshape and self.issue_list:
191
+ return None
192
+ elif self.metadata.extension is ExtensionCategory.reshape:
193
+ # Reshape allows changes to views
194
+ return None
195
+
196
+ new_views = {view.as_id(): view for view in user_schema.views}
197
+ existing_views = {view.as_id(): view for view in ref_schema.views}
198
+ for view_id, view in new_views.items():
199
+ existing_view = existing_views.get(view_id)
200
+ if not existing_view or existing_view == view:
201
+ # No problem
202
+ continue
203
+ changed_attributes, changed_properties = self._changed_attributes_and_properties(
204
+ view.dump(), existing_view.dump()
205
+ )
206
+ self.issue_list.append(
207
+ issues.dms.ChangingViewError(
208
+ view_id=view_id,
209
+ changed_properties=changed_properties or None,
210
+ changed_attributes=changed_attributes or None,
211
+ )
212
+ )
213
+
214
+ def _validate_performance(self) -> None:
215
+ # we can only validate performance on complete schemas due to the need
216
+ # to access all the container mappings
217
+ if self.metadata.schema_ is not SchemaCompleteness.complete:
218
+ return None
219
+
220
+ dms_schema = self.rules.as_schema()
221
+
222
+ for view in dms_schema.views:
223
+ mapped_containers = dms_schema._get_mapped_container_from_view(view.as_id())
224
+
225
+ if mapped_containers and len(mapped_containers) > 10:
226
+ self.issue_list.append(
227
+ issues.dms.ViewMapsToTooManyContainersWarning(
228
+ view_id=view.as_id(),
229
+ container_ids=mapped_containers,
230
+ )
231
+ )
232
+ if (
233
+ view.filter
234
+ and isinstance(view.filter, dm.filters.HasData)
235
+ and len(view.filter.dump()["hasData"]) > 10
236
+ ):
237
+ self.issue_list.append(
238
+ issues.dms.HasDataFilterAppliedToTooManyContainersWarning(
239
+ view_id=view.as_id(),
240
+ container_ids=mapped_containers,
241
+ )
242
+ )
243
+
244
+ @staticmethod
245
+ def _changed_attributes_and_properties(
246
+ new_dumped: dict[str, Any], existing_dumped: dict[str, Any]
247
+ ) -> tuple[list[str], list[str]]:
248
+ """Helper method to find the changed attributes and properties between two containers or views."""
249
+ new_attributes = {key: value for key, value in new_dumped.items() if key != "properties"}
250
+ existing_attributes = {key: value for key, value in existing_dumped.items() if key != "properties"}
251
+ changed_attributes = [key for key in new_attributes if new_attributes[key] != existing_attributes.get(key)]
252
+ new_properties = new_dumped.get("properties", {})
253
+ existing_properties = existing_dumped.get("properties", {})
254
+ changed_properties = [prop for prop in new_properties if new_properties[prop] != existing_properties.get(prop)]
255
+ return changed_attributes, changed_properties
256
+
257
+ def _validate_schema(self) -> None:
258
+ if self.metadata.schema_ is SchemaCompleteness.partial:
259
+ return None
260
+ elif self.metadata.schema_ is SchemaCompleteness.complete:
261
+ rules: DMSRules = self.rules
262
+ elif self.metadata.schema_ is SchemaCompleteness.extended:
263
+ if not self.rules.reference:
264
+ raise ValueError(
265
+ "The schema is set to 'extended', but no reference rules are provided to validate against"
266
+ )
267
+ # This is an extension of the reference rules, we need to merge the two
268
+ rules = self.rules.model_copy(deep=True)
269
+ rules.properties.extend(self.rules.reference.properties.data)
270
+ existing_views = {view.view.as_id() for view in rules.views}
271
+ rules.views.extend([view for view in self.rules.reference.views if view.view.as_id() not in existing_views])
272
+ if rules.containers and self.rules.reference.containers:
273
+ existing_containers = {container.container.as_id() for container in rules.containers.data}
274
+ rules.containers.extend(
275
+ [
276
+ container
277
+ for container in self.rules.reference.containers
278
+ if container.container.as_id() not in existing_containers
279
+ ]
280
+ )
281
+ elif not rules.containers and self.rules.reference.containers:
282
+ rules.containers = self.rules.reference.containers
283
+ else:
284
+ raise ValueError("Unknown schema completeness")
285
+
286
+ schema = rules.as_schema()
287
+ errors = schema.validate()
288
+ self.issue_list.extend(errors)
@@ -55,6 +55,7 @@ class DomainRules(RuleModel):
55
55
  metadata: DomainMetadata = Field(alias="Metadata")
56
56
  properties: SheetList[DomainProperty] = Field(alias="Properties")
57
57
  classes: SheetList[DomainClass] | None = Field(None, alias="Classes")
58
+ last: "DomainRules | None" = Field(None, alias="Last")
58
59
  reference: "DomainRules | None" = Field(None, alias="Reference")
59
60
 
60
61
  @model_serializer(mode="plain", when_used="always")
@@ -0,0 +1,3 @@
1
+ from ._rules import InformationClass, InformationMetadata, InformationProperty, InformationRules
2
+
3
+ __all__ = ["InformationRules", "InformationMetadata", "InformationClass", "InformationProperty"]
@@ -0,0 +1,195 @@
1
+ import re
2
+ from collections import defaultdict
3
+ from datetime import datetime
4
+ from typing import Literal
5
+
6
+ from cognite.neat.rules.models._base import (
7
+ DataModelType,
8
+ SheetList,
9
+ )
10
+ from cognite.neat.rules.models.data_types import DataType
11
+ from cognite.neat.rules.models.dms._rules import DMSProperty, DMSRules, DMSView
12
+ from cognite.neat.rules.models.domain import DomainRules
13
+ from cognite.neat.rules.models.entities import (
14
+ ClassEntity,
15
+ ContainerEntity,
16
+ DMSUnknownEntity,
17
+ ReferenceEntity,
18
+ UnknownEntity,
19
+ ViewEntity,
20
+ ViewPropertyEntity,
21
+ )
22
+
23
+ from ._rules import InformationClass, InformationMetadata, InformationProperty, InformationRules
24
+
25
+
26
+ class _InformationRulesConverter:
27
+ def __init__(self, information: InformationRules):
28
+ self.information = information
29
+
30
+ def as_domain_rules(self) -> DomainRules:
31
+ raise NotImplementedError("DomainRules not implemented yet")
32
+
33
+ def as_dms_architect_rules(self, created: datetime | None = None, updated: datetime | None = None) -> "DMSRules":
34
+ from cognite.neat.rules.models.dms._rules import (
35
+ DMSContainer,
36
+ DMSMetadata,
37
+ DMSProperty,
38
+ DMSRules,
39
+ )
40
+
41
+ info_metadata = self.information.metadata
42
+ default_version = info_metadata.version
43
+ default_space = self._to_space(info_metadata.prefix)
44
+ space = self._to_space(info_metadata.prefix)
45
+
46
+ metadata = DMSMetadata(
47
+ schema_=info_metadata.schema_,
48
+ space=space,
49
+ data_model_type=DataModelType.solution if self.information.reference else DataModelType.enterprise,
50
+ version=info_metadata.version,
51
+ external_id=info_metadata.name.replace(" ", "_").lower(),
52
+ creator=info_metadata.creator,
53
+ name=info_metadata.name,
54
+ created=created or datetime.now(),
55
+ updated=updated or datetime.now(),
56
+ )
57
+
58
+ properties_by_class: dict[str, list[DMSProperty]] = defaultdict(list)
59
+ for prop in self.information.properties:
60
+ properties_by_class[prop.class_.versioned_id].append(
61
+ self._as_dms_property(prop, default_space, default_version)
62
+ )
63
+
64
+ views: list[DMSView] = [
65
+ DMSView(
66
+ class_=cls_.class_,
67
+ name=cls_.name,
68
+ view=cls_.class_.as_view_entity(default_space, default_version),
69
+ description=cls_.description,
70
+ reference=cls_.reference,
71
+ implements=self._get_view_implements(cls_, info_metadata),
72
+ )
73
+ for cls_ in self.information.classes
74
+ ]
75
+
76
+ classes_without_properties: set[str] = set()
77
+ for class_ in self.information.classes:
78
+ properties: list[DMSProperty] = properties_by_class.get(class_.class_.versioned_id, [])
79
+ if not properties or all(
80
+ isinstance(prop.value_type, ViewPropertyEntity) and prop.connection != "direct" for prop in properties
81
+ ):
82
+ classes_without_properties.add(class_.class_.versioned_id)
83
+
84
+ containers: list[DMSContainer] = []
85
+ for class_ in self.information.classes:
86
+ if class_.class_.versioned_id in classes_without_properties:
87
+ continue
88
+ containers.append(
89
+ DMSContainer(
90
+ class_=class_.class_,
91
+ name=class_.name,
92
+ container=class_.class_.as_container_entity(default_space),
93
+ description=class_.description,
94
+ constraint=[
95
+ parent.as_container_entity(default_space)
96
+ for parent in class_.parent or []
97
+ if parent.versioned_id not in classes_without_properties
98
+ ]
99
+ or None,
100
+ )
101
+ )
102
+
103
+ return DMSRules(
104
+ metadata=metadata,
105
+ properties=SheetList[DMSProperty](
106
+ data=[prop for prop_set in properties_by_class.values() for prop in prop_set]
107
+ ),
108
+ views=SheetList[DMSView](data=views),
109
+ containers=SheetList[DMSContainer](data=containers),
110
+ last=self.information.last.as_dms_architect_rules() if self.information.last else None,
111
+ reference=self.information.reference.as_dms_architect_rules() if self.information.reference else None,
112
+ )
113
+
114
+ @classmethod
115
+ def _as_dms_property(cls, prop: InformationProperty, default_space: str, default_version: str) -> "DMSProperty":
116
+ """This creates the first"""
117
+
118
+ from cognite.neat.rules.models.dms._rules import DMSProperty
119
+
120
+ # returns property type, which can be ObjectProperty or DatatypeProperty
121
+ value_type: DataType | ViewEntity | ViewPropertyEntity | DMSUnknownEntity
122
+ if isinstance(prop.value_type, DataType):
123
+ value_type = prop.value_type
124
+ elif isinstance(prop.value_type, UnknownEntity):
125
+ value_type = DMSUnknownEntity()
126
+ elif isinstance(prop.value_type, ClassEntity):
127
+ value_type = prop.value_type.as_view_entity(default_space, default_version)
128
+ else:
129
+ raise ValueError(f"Unsupported value type: {prop.value_type.type_}")
130
+
131
+ relation: Literal["direct", "edge", "reverse"] | None = None
132
+ if isinstance(value_type, ViewEntity | ViewPropertyEntity):
133
+ relation = "edge" if prop.is_list else "direct"
134
+
135
+ container: ContainerEntity | None = None
136
+ container_property: str | None = None
137
+ is_list: bool | None = prop.is_list
138
+ nullable: bool | None = not prop.is_mandatory
139
+ if relation == "edge":
140
+ nullable = None
141
+ elif relation == "direct":
142
+ nullable = True
143
+ container, container_property = cls._get_container(prop, default_space)
144
+ else:
145
+ container, container_property = cls._get_container(prop, default_space)
146
+
147
+ return DMSProperty(
148
+ class_=prop.class_,
149
+ name=prop.name,
150
+ property_=prop.property_,
151
+ value_type=value_type,
152
+ nullable=nullable,
153
+ is_list=is_list,
154
+ connection=relation,
155
+ default=prop.default,
156
+ reference=prop.reference,
157
+ container=container,
158
+ container_property=container_property,
159
+ view=prop.class_.as_view_entity(default_space, default_version),
160
+ view_property=prop.property_,
161
+ )
162
+
163
+ @classmethod
164
+ def _to_space(cls, prefix: str) -> str:
165
+ """Ensures that the prefix comply with the CDF space regex"""
166
+ prefix = re.sub(r"[^a-zA-Z0-9_-]", "_", prefix)
167
+ if prefix[0].isdigit() or prefix[0] == "_":
168
+ prefix = f"a{prefix}"
169
+ prefix = prefix[:43]
170
+ if prefix[-1] == "_":
171
+ prefix = f"{prefix[:-1]}1"
172
+ return prefix
173
+
174
+ @classmethod
175
+ def _get_container(cls, prop: InformationProperty, default_space: str) -> tuple[ContainerEntity, str]:
176
+ if isinstance(prop.reference, ReferenceEntity):
177
+ return (
178
+ prop.reference.as_container_entity(default_space),
179
+ prop.reference.property_ or prop.property_,
180
+ )
181
+ else:
182
+ return prop.class_.as_container_entity(default_space), prop.property_
183
+
184
+ @classmethod
185
+ def _get_view_implements(cls, cls_: InformationClass, metadata: InformationMetadata) -> list[ViewEntity]:
186
+ if isinstance(cls_.reference, ReferenceEntity) and cls_.reference.prefix != metadata.prefix:
187
+ # We use the reference for implements if it is in a different namespace
188
+ implements = [
189
+ cls_.reference.as_view_entity(metadata.prefix, metadata.version),
190
+ ]
191
+ else:
192
+ implements = []
193
+
194
+ implements.extend([parent.as_view_entity(metadata.prefix, metadata.version) for parent in cls_.parent or []])
195
+ return implements