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
|
@@ -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,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
|