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.
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} +33 -33
  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.1.dist-info → cognite_neat-0.76.2.dist-info}/METADATA +1 -1
  41. {cognite_neat-0.76.1.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.1.dist-info → cognite_neat-0.76.2.dist-info}/LICENSE +0 -0
  51. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/WHEEL +0 -0
  52. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.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 self.container_name in dumped:
81
+ dumped[self.container_name] = sorted(
82
+ dumped[self.container_name]["data"], key=lambda c: c[self.container_container]
83
+ )
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[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,255 @@
1
+ from collections import defaultdict
2
+ from typing import Any
3
+
4
+ from cognite.neat.rules import issues
5
+ from cognite.neat.rules.issues import IssueList
6
+ from cognite.neat.rules.models._base import ExtensionCategory, SchemaCompleteness
7
+ from cognite.neat.rules.models.data_types import DataType
8
+ from cognite.neat.rules.models.entities import ContainerEntity
9
+
10
+ from ._rules import DMSProperty, DMSRules
11
+
12
+
13
+ class DMSPostValidation:
14
+ """This class does all the validation of the DMS rules that have dependencies between
15
+ components."""
16
+
17
+ def __init__(self, rules: DMSRules):
18
+ self.rules = rules
19
+ self.metadata = rules.metadata
20
+ self.properties = rules.properties
21
+ self.containers = rules.containers
22
+ self.views = rules.views
23
+ self.issue_list = IssueList()
24
+
25
+ def validate(self) -> IssueList:
26
+ self._consistent_container_properties()
27
+ self._referenced_views_and_containers_are_existing()
28
+ self._validate_extension()
29
+ self._validate_schema()
30
+ return self.issue_list
31
+
32
+ def _consistent_container_properties(self) -> None:
33
+ container_properties_by_id: dict[tuple[ContainerEntity, str], list[tuple[int, DMSProperty]]] = defaultdict(list)
34
+ for prop_no, prop in enumerate(self.properties):
35
+ if prop.container and prop.container_property:
36
+ container_properties_by_id[(prop.container, prop.container_property)].append((prop_no, prop))
37
+
38
+ errors: list[issues.spreadsheet.InconsistentContainerDefinitionError] = []
39
+ for (container, prop_name), properties in container_properties_by_id.items():
40
+ if len(properties) == 1:
41
+ continue
42
+ container_id = container.as_id()
43
+ row_numbers = {prop_no for prop_no, _ in properties}
44
+ value_types = {prop.value_type for _, prop in properties if prop.value_type}
45
+ if len(value_types) > 1:
46
+ errors.append(
47
+ issues.spreadsheet.MultiValueTypeError(
48
+ container_id,
49
+ prop_name,
50
+ row_numbers,
51
+ {v.dms._type if isinstance(v, DataType) else str(v) for v in value_types},
52
+ )
53
+ )
54
+ list_definitions = {prop.is_list for _, prop in properties if prop.is_list is not None}
55
+ if len(list_definitions) > 1:
56
+ errors.append(
57
+ issues.spreadsheet.MultiValueIsListError(container_id, prop_name, row_numbers, list_definitions)
58
+ )
59
+ nullable_definitions = {prop.nullable for _, prop in properties if prop.nullable is not None}
60
+ if len(nullable_definitions) > 1:
61
+ errors.append(
62
+ issues.spreadsheet.MultiNullableError(container_id, prop_name, row_numbers, nullable_definitions)
63
+ )
64
+ default_definitions = {prop.default for _, prop in properties if prop.default is not None}
65
+ if len(default_definitions) > 1:
66
+ errors.append(
67
+ issues.spreadsheet.MultiDefaultError(
68
+ container_id, prop_name, row_numbers, list(default_definitions)
69
+ )
70
+ )
71
+ index_definitions = {",".join(prop.index) for _, prop in properties if prop.index is not None}
72
+ if len(index_definitions) > 1:
73
+ errors.append(
74
+ issues.spreadsheet.MultiIndexError(container_id, prop_name, row_numbers, index_definitions)
75
+ )
76
+ constraint_definitions = {
77
+ ",".join(prop.constraint) for _, prop in properties if prop.constraint is not None
78
+ }
79
+ if len(constraint_definitions) > 1:
80
+ errors.append(
81
+ issues.spreadsheet.MultiUniqueConstraintError(
82
+ container_id, prop_name, row_numbers, constraint_definitions
83
+ )
84
+ )
85
+
86
+ # This sets the container definition for all the properties where it is not defined.
87
+ # This allows the user to define the container only once.
88
+ value_type = next(iter(value_types))
89
+ list_definition = next(iter(list_definitions)) if list_definitions else None
90
+ nullable_definition = next(iter(nullable_definitions)) if nullable_definitions else None
91
+ default_definition = next(iter(default_definitions)) if default_definitions else None
92
+ index_definition = next(iter(index_definitions)).split(",") if index_definitions else None
93
+ constraint_definition = next(iter(constraint_definitions)).split(",") if constraint_definitions else None
94
+ for _, prop in properties:
95
+ prop.value_type = value_type
96
+ prop.is_list = prop.is_list or list_definition
97
+ prop.nullable = prop.nullable or nullable_definition
98
+ prop.default = prop.default or default_definition
99
+ prop.index = prop.index or index_definition
100
+ prop.constraint = prop.constraint or constraint_definition
101
+ self.issue_list.extend(errors)
102
+
103
+ def _referenced_views_and_containers_are_existing(self) -> None:
104
+ # There two checks are done in the same method to raise all the errors at once.
105
+ defined_views = {view.view.as_id() for view in self.views}
106
+
107
+ errors: list[issues.NeatValidationError] = []
108
+ for prop_no, prop in enumerate(self.properties):
109
+ if prop.view and (view_id := prop.view.as_id()) not in defined_views:
110
+ errors.append(
111
+ issues.spreadsheet.NonExistingViewError(
112
+ column="View",
113
+ row=prop_no,
114
+ type="value_error.missing",
115
+ view_id=view_id,
116
+ msg="",
117
+ input=None,
118
+ url=None,
119
+ )
120
+ )
121
+ if self.metadata.schema_ is SchemaCompleteness.complete:
122
+ defined_containers = {container.container.as_id() for container in self.containers or []}
123
+ for prop_no, prop in enumerate(self.properties):
124
+ if prop.container and (container_id := prop.container.as_id()) not in defined_containers:
125
+ errors.append(
126
+ issues.spreadsheet.NonExistingContainerError(
127
+ column="Container",
128
+ row=prop_no,
129
+ type="value_error.missing",
130
+ container_id=container_id,
131
+ msg="",
132
+ input=None,
133
+ url=None,
134
+ )
135
+ )
136
+ for _container_no, container in enumerate(self.containers or []):
137
+ for constraint_no, constraint in enumerate(container.constraint or []):
138
+ if constraint.as_id() not in defined_containers:
139
+ errors.append(
140
+ issues.spreadsheet.NonExistingContainerError(
141
+ column="Constraint",
142
+ row=constraint_no,
143
+ type="value_error.missing",
144
+ container_id=constraint.as_id(),
145
+ msg="",
146
+ input=None,
147
+ url=None,
148
+ )
149
+ )
150
+ self.issue_list.extend(errors)
151
+
152
+ def _validate_extension(self) -> None:
153
+ if self.metadata.schema_ is not SchemaCompleteness.extended:
154
+ return None
155
+ if not self.rules.reference:
156
+ raise ValueError("The schema is set to 'extended', but no reference rules are provided to validate against")
157
+ is_solution = self.metadata.space != self.rules.reference.metadata.space
158
+ if is_solution:
159
+ return None
160
+ if self.metadata.extension is ExtensionCategory.rebuild:
161
+ # Everything is allowed
162
+ return None
163
+ # Is an extension of an existing model.
164
+ user_schema = self.rules.as_schema(include_ref=False)
165
+ ref_schema = self.rules.reference.as_schema()
166
+ new_containers = {container.as_id(): container for container in user_schema.containers}
167
+ existing_containers = {container.as_id(): container for container in ref_schema.containers}
168
+
169
+ for container_id, container in new_containers.items():
170
+ existing_container = existing_containers.get(container_id)
171
+ if not existing_container or existing_container == container:
172
+ # No problem
173
+ continue
174
+ new_dumped = container.dump()
175
+ existing_dumped = existing_container.dump()
176
+ changed_attributes, changed_properties = self._changed_attributes_and_properties(
177
+ new_dumped, existing_dumped
178
+ )
179
+ self.issue_list.append(
180
+ issues.dms.ChangingContainerError(
181
+ container_id=container_id,
182
+ changed_properties=changed_properties or None,
183
+ changed_attributes=changed_attributes or None,
184
+ )
185
+ )
186
+
187
+ if self.metadata.extension is ExtensionCategory.reshape and self.issue_list:
188
+ return None
189
+ elif self.metadata.extension is ExtensionCategory.reshape:
190
+ # Reshape allows changes to views
191
+ return None
192
+
193
+ new_views = {view.as_id(): view for view in user_schema.views}
194
+ existing_views = {view.as_id(): view for view in ref_schema.views}
195
+ for view_id, view in new_views.items():
196
+ existing_view = existing_views.get(view_id)
197
+ if not existing_view or existing_view == view:
198
+ # No problem
199
+ continue
200
+ changed_attributes, changed_properties = self._changed_attributes_and_properties(
201
+ view.dump(), existing_view.dump()
202
+ )
203
+ self.issue_list.append(
204
+ issues.dms.ChangingViewError(
205
+ view_id=view_id,
206
+ changed_properties=changed_properties or None,
207
+ changed_attributes=changed_attributes or None,
208
+ )
209
+ )
210
+
211
+ @staticmethod
212
+ def _changed_attributes_and_properties(
213
+ new_dumped: dict[str, Any], existing_dumped: dict[str, Any]
214
+ ) -> tuple[list[str], list[str]]:
215
+ """Helper method to find the changed attributes and properties between two containers or views."""
216
+ new_attributes = {key: value for key, value in new_dumped.items() if key != "properties"}
217
+ existing_attributes = {key: value for key, value in existing_dumped.items() if key != "properties"}
218
+ changed_attributes = [key for key in new_attributes if new_attributes[key] != existing_attributes.get(key)]
219
+ new_properties = new_dumped.get("properties", {})
220
+ existing_properties = existing_dumped.get("properties", {})
221
+ changed_properties = [prop for prop in new_properties if new_properties[prop] != existing_properties.get(prop)]
222
+ return changed_attributes, changed_properties
223
+
224
+ def _validate_schema(self) -> None:
225
+ if self.metadata.schema_ is SchemaCompleteness.partial:
226
+ return None
227
+ elif self.metadata.schema_ is SchemaCompleteness.complete:
228
+ rules: DMSRules = self.rules
229
+ elif self.metadata.schema_ is SchemaCompleteness.extended:
230
+ if not self.rules.reference:
231
+ raise ValueError(
232
+ "The schema is set to 'extended', but no reference rules are provided to validate against"
233
+ )
234
+ # This is an extension of the reference rules, we need to merge the two
235
+ rules = self.rules.model_copy(deep=True)
236
+ rules.properties.extend(self.rules.reference.properties.data)
237
+ existing_views = {view.view.as_id() for view in rules.views}
238
+ rules.views.extend([view for view in self.rules.reference.views if view.view.as_id() not in existing_views])
239
+ if rules.containers and self.rules.reference.containers:
240
+ existing_containers = {container.container.as_id() for container in rules.containers.data}
241
+ rules.containers.extend(
242
+ [
243
+ container
244
+ for container in self.rules.reference.containers
245
+ if container.container.as_id() not in existing_containers
246
+ ]
247
+ )
248
+ elif not rules.containers and self.rules.reference.containers:
249
+ rules.containers = self.rules.reference.containers
250
+ else:
251
+ raise ValueError("Unknown schema completeness")
252
+
253
+ schema = rules.as_schema()
254
+ errors = schema.validate()
255
+ self.issue_list.extend(errors)
@@ -0,0 +1,3 @@
1
+ from ._rules import InformationClass, InformationMetadata, InformationProperty, InformationRules
2
+
3
+ __all__ = ["InformationRules", "InformationMetadata", "InformationClass", "InformationProperty"]
@@ -0,0 +1,193 @@
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
+ SheetList,
8
+ )
9
+ from cognite.neat.rules.models.data_types import DataType
10
+ from cognite.neat.rules.models.dms._rules import DMSProperty, DMSRules, DMSView
11
+ from cognite.neat.rules.models.domain import DomainRules
12
+ from cognite.neat.rules.models.entities import (
13
+ ClassEntity,
14
+ ContainerEntity,
15
+ DMSUnknownEntity,
16
+ ReferenceEntity,
17
+ UnknownEntity,
18
+ ViewEntity,
19
+ ViewPropertyEntity,
20
+ )
21
+
22
+ from ._rules import InformationClass, InformationMetadata, InformationProperty, InformationRules
23
+
24
+
25
+ class _InformationRulesConverter:
26
+ def __init__(self, information: InformationRules):
27
+ self.information = information
28
+
29
+ def as_domain_rules(self) -> DomainRules:
30
+ raise NotImplementedError("DomainRules not implemented yet")
31
+
32
+ def as_dms_architect_rules(self, created: datetime | None = None, updated: datetime | None = None) -> "DMSRules":
33
+ from cognite.neat.rules.models.dms._rules import (
34
+ DMSContainer,
35
+ DMSMetadata,
36
+ DMSProperty,
37
+ DMSRules,
38
+ )
39
+
40
+ info_metadata = self.information.metadata
41
+ default_version = info_metadata.version
42
+ default_space = self._to_space(info_metadata.prefix)
43
+ space = self._to_space(info_metadata.prefix)
44
+
45
+ metadata = DMSMetadata(
46
+ schema_=info_metadata.schema_,
47
+ space=space,
48
+ version=info_metadata.version,
49
+ external_id=info_metadata.name.replace(" ", "_").lower(),
50
+ creator=info_metadata.creator,
51
+ name=info_metadata.name,
52
+ created=created or datetime.now(),
53
+ updated=updated or datetime.now(),
54
+ )
55
+
56
+ properties_by_class: dict[str, list[DMSProperty]] = defaultdict(list)
57
+ for prop in self.information.properties:
58
+ properties_by_class[prop.class_.versioned_id].append(
59
+ self._as_dms_property(prop, default_space, default_version)
60
+ )
61
+
62
+ views: list[DMSView] = [
63
+ DMSView(
64
+ class_=cls_.class_,
65
+ name=cls_.name,
66
+ view=cls_.class_.as_view_entity(default_space, default_version),
67
+ description=cls_.description,
68
+ reference=cls_.reference,
69
+ implements=self._get_view_implements(cls_, info_metadata),
70
+ )
71
+ for cls_ in self.information.classes
72
+ ]
73
+
74
+ classes_without_properties: set[str] = set()
75
+ for class_ in self.information.classes:
76
+ properties: list[DMSProperty] = properties_by_class.get(class_.class_.versioned_id, [])
77
+ if not properties or all(
78
+ isinstance(prop.value_type, ViewPropertyEntity) and prop.connection != "direct" for prop in properties
79
+ ):
80
+ classes_without_properties.add(class_.class_.versioned_id)
81
+
82
+ containers: list[DMSContainer] = []
83
+ for class_ in self.information.classes:
84
+ if class_.class_.versioned_id in classes_without_properties:
85
+ continue
86
+ containers.append(
87
+ DMSContainer(
88
+ class_=class_.class_,
89
+ name=class_.name,
90
+ container=class_.class_.as_container_entity(default_space),
91
+ description=class_.description,
92
+ constraint=[
93
+ parent.as_container_entity(default_space)
94
+ for parent in class_.parent or []
95
+ if parent.versioned_id not in classes_without_properties
96
+ ]
97
+ or None,
98
+ )
99
+ )
100
+
101
+ return DMSRules(
102
+ metadata=metadata,
103
+ properties=SheetList[DMSProperty](
104
+ data=[prop for prop_set in properties_by_class.values() for prop in prop_set]
105
+ ),
106
+ views=SheetList[DMSView](data=views),
107
+ containers=SheetList[DMSContainer](data=containers),
108
+ last=self.information.last.as_dms_architect_rules() if self.information.last else None,
109
+ reference=self.information.reference.as_dms_architect_rules() if self.information.reference else None,
110
+ )
111
+
112
+ @classmethod
113
+ def _as_dms_property(cls, prop: InformationProperty, default_space: str, default_version: str) -> "DMSProperty":
114
+ """This creates the first"""
115
+
116
+ from cognite.neat.rules.models.dms._rules import DMSProperty
117
+
118
+ # returns property type, which can be ObjectProperty or DatatypeProperty
119
+ value_type: DataType | ViewEntity | ViewPropertyEntity | DMSUnknownEntity
120
+ if isinstance(prop.value_type, DataType):
121
+ value_type = prop.value_type
122
+ elif isinstance(prop.value_type, UnknownEntity):
123
+ value_type = DMSUnknownEntity()
124
+ elif isinstance(prop.value_type, ClassEntity):
125
+ value_type = prop.value_type.as_view_entity(default_space, default_version)
126
+ else:
127
+ raise ValueError(f"Unsupported value type: {prop.value_type.type_}")
128
+
129
+ relation: Literal["direct", "edge", "reverse"] | None = None
130
+ if isinstance(value_type, ViewEntity | ViewPropertyEntity):
131
+ relation = "edge" if prop.is_list else "direct"
132
+
133
+ container: ContainerEntity | None = None
134
+ container_property: str | None = None
135
+ is_list: bool | None = prop.is_list
136
+ nullable: bool | None = not prop.is_mandatory
137
+ if relation == "edge":
138
+ nullable = None
139
+ elif relation == "direct":
140
+ nullable = True
141
+ container, container_property = cls._get_container(prop, default_space)
142
+ else:
143
+ container, container_property = cls._get_container(prop, default_space)
144
+
145
+ return DMSProperty(
146
+ class_=prop.class_,
147
+ name=prop.name,
148
+ property_=prop.property_,
149
+ value_type=value_type,
150
+ nullable=nullable,
151
+ is_list=is_list,
152
+ connection=relation,
153
+ default=prop.default,
154
+ reference=prop.reference,
155
+ container=container,
156
+ container_property=container_property,
157
+ view=prop.class_.as_view_entity(default_space, default_version),
158
+ view_property=prop.property_,
159
+ )
160
+
161
+ @classmethod
162
+ def _to_space(cls, prefix: str) -> str:
163
+ """Ensures that the prefix comply with the CDF space regex"""
164
+ prefix = re.sub(r"[^a-zA-Z0-9_-]", "_", prefix)
165
+ if prefix[0].isdigit() or prefix[0] == "_":
166
+ prefix = f"a{prefix}"
167
+ prefix = prefix[:43]
168
+ if prefix[-1] == "_":
169
+ prefix = f"{prefix[:-1]}1"
170
+ return prefix
171
+
172
+ @classmethod
173
+ def _get_container(cls, prop: InformationProperty, default_space: str) -> tuple[ContainerEntity, str]:
174
+ if isinstance(prop.reference, ReferenceEntity):
175
+ return (
176
+ prop.reference.as_container_entity(default_space),
177
+ prop.reference.property_ or prop.property_,
178
+ )
179
+ else:
180
+ return prop.class_.as_container_entity(default_space), prop.property_
181
+
182
+ @classmethod
183
+ def _get_view_implements(cls, cls_: InformationClass, metadata: InformationMetadata) -> list[ViewEntity]:
184
+ if isinstance(cls_.reference, ReferenceEntity) and cls_.reference.prefix != metadata.prefix:
185
+ # We use the reference for implements if it is in a different namespace
186
+ implements = [
187
+ cls_.reference.as_view_entity(metadata.prefix, metadata.version),
188
+ ]
189
+ else:
190
+ implements = []
191
+
192
+ implements.extend([parent.as_view_entity(metadata.prefix, metadata.version) for parent in cls_.parent or []])
193
+ return implements