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.
- 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 +71 -40
- 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 +87 -62
- cognite/neat/rules/importers/_yaml2rules.py +3 -3
- cognite/neat/rules/issues/base.py +5 -0
- cognite/neat/rules/issues/dms.py +65 -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} +42 -33
- cognite/neat/rules/models/{rules/_dms_schema.py → dms/_schema.py} +36 -4
- cognite/neat/rules/models/dms/_serializer.py +126 -0
- cognite/neat/rules/models/dms/_validation.py +288 -0
- cognite/neat/rules/models/{rules/_domain_rules.py → domain.py} +1 -0
- cognite/neat/rules/models/information/__init__.py +3 -0
- cognite/neat/rules/models/information/_converter.py +195 -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 +10 -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.3.dist-info}/METADATA +1 -1
- {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.3.dist-info}/RECORD +51 -44
- 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-0.76.1.dist-info → cognite_neat-0.76.3.dist-info}/LICENSE +0 -0
- {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.3.dist-info}/WHEEL +0 -0
- {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import warnings
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast
|
|
8
|
+
|
|
9
|
+
from cognite.client import data_modeling as dm
|
|
10
|
+
from pydantic import Field, field_serializer, field_validator, model_serializer, model_validator
|
|
11
|
+
from pydantic_core.core_schema import SerializationInfo, ValidationInfo
|
|
12
|
+
|
|
13
|
+
from cognite.neat.rules import issues
|
|
14
|
+
from cognite.neat.rules.issues import MultiValueError
|
|
15
|
+
from cognite.neat.rules.models._base import (
|
|
16
|
+
BaseMetadata,
|
|
17
|
+
BaseRules,
|
|
18
|
+
DataModelType,
|
|
19
|
+
ExtensionCategory,
|
|
20
|
+
RoleTypes,
|
|
21
|
+
SchemaCompleteness,
|
|
22
|
+
SheetEntity,
|
|
23
|
+
SheetList,
|
|
24
|
+
)
|
|
25
|
+
from cognite.neat.rules.models._types import (
|
|
26
|
+
ExternalIdType,
|
|
27
|
+
PropertyType,
|
|
28
|
+
StrListType,
|
|
29
|
+
VersionType,
|
|
30
|
+
)
|
|
31
|
+
from cognite.neat.rules.models.data_types import DataType
|
|
32
|
+
from cognite.neat.rules.models.domain import DomainRules
|
|
33
|
+
from cognite.neat.rules.models.entities import (
|
|
34
|
+
ClassEntity,
|
|
35
|
+
ContainerEntity,
|
|
36
|
+
ContainerEntityList,
|
|
37
|
+
DMSUnknownEntity,
|
|
38
|
+
ReferenceEntity,
|
|
39
|
+
URLEntity,
|
|
40
|
+
ViewEntity,
|
|
41
|
+
ViewEntityList,
|
|
42
|
+
ViewPropertyEntity,
|
|
43
|
+
)
|
|
44
|
+
from cognite.neat.rules.models.wrapped_entities import HasDataFilter, NodeTypeFilter
|
|
45
|
+
|
|
46
|
+
from ._schema import DMSSchema
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from cognite.neat.rules.models.information._rules import InformationRules
|
|
50
|
+
|
|
51
|
+
if sys.version_info >= (3, 11):
|
|
52
|
+
from typing import Self
|
|
53
|
+
else:
|
|
54
|
+
from typing_extensions import Self
|
|
55
|
+
|
|
56
|
+
_DEFAULT_VERSION = "1"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DMSMetadata(BaseMetadata):
|
|
60
|
+
role: ClassVar[RoleTypes] = RoleTypes.dms_architect
|
|
61
|
+
data_model_type: DataModelType = Field(DataModelType.solution, alias="dataModelType")
|
|
62
|
+
schema_: SchemaCompleteness = Field(alias="schema")
|
|
63
|
+
extension: ExtensionCategory = ExtensionCategory.addition
|
|
64
|
+
space: ExternalIdType
|
|
65
|
+
name: str | None = Field(
|
|
66
|
+
None,
|
|
67
|
+
description="Human readable name of the data model",
|
|
68
|
+
min_length=1,
|
|
69
|
+
max_length=255,
|
|
70
|
+
)
|
|
71
|
+
description: str | None = Field(None, min_length=1, max_length=1024)
|
|
72
|
+
external_id: ExternalIdType = Field(alias="externalId")
|
|
73
|
+
version: VersionType
|
|
74
|
+
creator: StrListType
|
|
75
|
+
created: datetime = Field(
|
|
76
|
+
description=("Date of the data model creation"),
|
|
77
|
+
)
|
|
78
|
+
updated: datetime = Field(
|
|
79
|
+
description=("Date of the data model update"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@field_validator("*", mode="before")
|
|
83
|
+
def strip_string(cls, value: Any) -> Any:
|
|
84
|
+
if isinstance(value, str):
|
|
85
|
+
return value.strip()
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
@field_serializer("schema_", "extension", "data_model_type", when_used="always")
|
|
89
|
+
@staticmethod
|
|
90
|
+
def as_string(value: SchemaCompleteness | ExtensionCategory | DataModelType) -> str:
|
|
91
|
+
return str(value)
|
|
92
|
+
|
|
93
|
+
@field_validator("schema_", mode="plain")
|
|
94
|
+
def as_enum_schema(cls, value: str) -> SchemaCompleteness:
|
|
95
|
+
return SchemaCompleteness(value)
|
|
96
|
+
|
|
97
|
+
@field_validator("extension", mode="plain")
|
|
98
|
+
def as_enum_extension(cls, value: str) -> ExtensionCategory:
|
|
99
|
+
return ExtensionCategory(value)
|
|
100
|
+
|
|
101
|
+
@field_validator("data_model_type", mode="plain")
|
|
102
|
+
def as_enum_model_type(cls, value: str) -> DataModelType:
|
|
103
|
+
return DataModelType(value)
|
|
104
|
+
|
|
105
|
+
@field_validator("description", mode="before")
|
|
106
|
+
def nan_as_none(cls, value):
|
|
107
|
+
if isinstance(value, float) and math.isnan(value):
|
|
108
|
+
return None
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
def as_space(self) -> dm.SpaceApply:
|
|
112
|
+
return dm.SpaceApply(
|
|
113
|
+
space=self.space,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def as_data_model_id(self) -> dm.DataModelId:
|
|
117
|
+
return dm.DataModelId(space=self.space, external_id=self.external_id, version=self.version)
|
|
118
|
+
|
|
119
|
+
def as_data_model(self) -> dm.DataModelApply:
|
|
120
|
+
suffix = f"Creator: {', '.join(self.creator)}"
|
|
121
|
+
if self.description:
|
|
122
|
+
description = f"{self.description} Creator: {', '.join(self.creator)}"
|
|
123
|
+
else:
|
|
124
|
+
description = suffix
|
|
125
|
+
|
|
126
|
+
return dm.DataModelApply(
|
|
127
|
+
space=self.space,
|
|
128
|
+
external_id=self.external_id,
|
|
129
|
+
name=self.name or None,
|
|
130
|
+
version=self.version or "missing",
|
|
131
|
+
description=description,
|
|
132
|
+
views=[],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def _get_description_and_creator(cls, description_raw: str | None) -> tuple[str | None, list[str]]:
|
|
137
|
+
if description_raw and (description_match := re.search(r"Creator: (.+)", description_raw)):
|
|
138
|
+
creator = description_match.group(1).split(", ")
|
|
139
|
+
description = description_raw.replace(description_match.string, "").strip() or None
|
|
140
|
+
elif description_raw:
|
|
141
|
+
creator = ["MISSING"]
|
|
142
|
+
description = description_raw
|
|
143
|
+
else:
|
|
144
|
+
creator = ["MISSING"]
|
|
145
|
+
description = None
|
|
146
|
+
return description, creator
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_data_model(cls, data_model: dm.DataModelApply) -> "DMSMetadata":
|
|
150
|
+
description, creator = cls._get_description_and_creator(data_model.description)
|
|
151
|
+
return cls(
|
|
152
|
+
schema_=SchemaCompleteness.complete,
|
|
153
|
+
space=data_model.space,
|
|
154
|
+
name=data_model.name or None,
|
|
155
|
+
description=description,
|
|
156
|
+
external_id=data_model.external_id,
|
|
157
|
+
version=data_model.version,
|
|
158
|
+
creator=creator,
|
|
159
|
+
created=datetime.now(),
|
|
160
|
+
updated=datetime.now(),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class DMSProperty(SheetEntity):
|
|
165
|
+
view: ViewEntity = Field(alias="View")
|
|
166
|
+
view_property: str = Field(alias="View Property")
|
|
167
|
+
name: str | None = Field(alias="Name", default=None)
|
|
168
|
+
description: str | None = Field(alias="Description", default=None)
|
|
169
|
+
connection: Literal["direct", "edge", "reverse"] | None = Field(None, alias="Connection")
|
|
170
|
+
value_type: DataType | ViewPropertyEntity | ViewEntity | DMSUnknownEntity = Field(alias="Value Type")
|
|
171
|
+
nullable: bool | None = Field(default=None, alias="Nullable")
|
|
172
|
+
is_list: bool | None = Field(default=None, alias="Is List")
|
|
173
|
+
default: str | int | dict | None = Field(None, alias="Default")
|
|
174
|
+
reference: URLEntity | ReferenceEntity | None = Field(default=None, alias="Reference", union_mode="left_to_right")
|
|
175
|
+
container: ContainerEntity | None = Field(None, alias="Container")
|
|
176
|
+
container_property: str | None = Field(None, alias="Container Property")
|
|
177
|
+
index: StrListType | None = Field(None, alias="Index")
|
|
178
|
+
constraint: StrListType | None = Field(None, alias="Constraint")
|
|
179
|
+
class_: ClassEntity = Field(alias="Class (linage)")
|
|
180
|
+
property_: PropertyType = Field(alias="Property (linage)")
|
|
181
|
+
|
|
182
|
+
@field_validator("nullable")
|
|
183
|
+
def direct_relation_must_be_nullable(cls, value: Any, info: ValidationInfo) -> None:
|
|
184
|
+
if info.data.get("connection") == "direct" and value is False:
|
|
185
|
+
raise ValueError("Direct relation must be nullable")
|
|
186
|
+
return value
|
|
187
|
+
|
|
188
|
+
@field_validator("value_type", mode="after")
|
|
189
|
+
def connections_value_type(
|
|
190
|
+
cls, value: ViewPropertyEntity | ViewEntity | DMSUnknownEntity, info: ValidationInfo
|
|
191
|
+
) -> DataType | ViewPropertyEntity | ViewEntity | DMSUnknownEntity:
|
|
192
|
+
if (connection := info.data.get("connection")) is None:
|
|
193
|
+
return value
|
|
194
|
+
if connection == "direct" and not isinstance(value, ViewEntity | DMSUnknownEntity):
|
|
195
|
+
raise ValueError(f"Direct relation must have a value type that points to a view, got {value}")
|
|
196
|
+
elif connection == "edge" and not isinstance(value, ViewEntity):
|
|
197
|
+
raise ValueError(f"Edge connection must have a value type that points to a view, got {value}")
|
|
198
|
+
elif connection == "reverse" and not isinstance(value, ViewPropertyEntity | ViewEntity):
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Reverse connection must have a value type that points to a view or view property, got {value}"
|
|
201
|
+
)
|
|
202
|
+
return value
|
|
203
|
+
|
|
204
|
+
@field_serializer("value_type", when_used="always")
|
|
205
|
+
@staticmethod
|
|
206
|
+
def as_dms_type(value_type: DataType | ViewPropertyEntity | ViewEntity) -> str:
|
|
207
|
+
if isinstance(value_type, DataType):
|
|
208
|
+
return value_type.dms._type
|
|
209
|
+
else:
|
|
210
|
+
return str(value_type)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class DMSContainer(SheetEntity):
|
|
214
|
+
container: ContainerEntity = Field(alias="Container")
|
|
215
|
+
name: str | None = Field(alias="Name", default=None)
|
|
216
|
+
description: str | None = Field(alias="Description", default=None)
|
|
217
|
+
reference: URLEntity | ReferenceEntity | None = Field(alias="Reference", default=None, union_mode="left_to_right")
|
|
218
|
+
constraint: ContainerEntityList | None = Field(None, alias="Constraint")
|
|
219
|
+
class_: ClassEntity = Field(alias="Class (linage)")
|
|
220
|
+
|
|
221
|
+
def as_container(self) -> dm.ContainerApply:
|
|
222
|
+
container_id = self.container.as_id()
|
|
223
|
+
constraints: dict[str, dm.Constraint] = {}
|
|
224
|
+
for constraint in self.constraint or []:
|
|
225
|
+
requires = dm.RequiresConstraint(constraint.as_id())
|
|
226
|
+
constraints[f"{constraint.space}_{constraint.external_id}"] = requires
|
|
227
|
+
|
|
228
|
+
return dm.ContainerApply(
|
|
229
|
+
space=container_id.space,
|
|
230
|
+
external_id=container_id.external_id,
|
|
231
|
+
name=self.name or None,
|
|
232
|
+
description=self.description,
|
|
233
|
+
constraints=constraints or None,
|
|
234
|
+
properties={},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def from_container(cls, container: dm.ContainerApply) -> "DMSContainer":
|
|
239
|
+
constraints: list[ContainerEntity] = []
|
|
240
|
+
for _, constraint_obj in (container.constraints or {}).items():
|
|
241
|
+
if isinstance(constraint_obj, dm.RequiresConstraint):
|
|
242
|
+
constraints.append(ContainerEntity.from_id(constraint_obj.require))
|
|
243
|
+
# UniquenessConstraint it handled in the properties
|
|
244
|
+
container_entity = ContainerEntity.from_id(container.as_id())
|
|
245
|
+
return cls(
|
|
246
|
+
class_=container_entity.as_class(),
|
|
247
|
+
container=container_entity,
|
|
248
|
+
name=container.name or None,
|
|
249
|
+
description=container.description,
|
|
250
|
+
constraint=constraints or None,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class DMSView(SheetEntity):
|
|
255
|
+
view: ViewEntity = Field(alias="View")
|
|
256
|
+
name: str | None = Field(alias="Name", default=None)
|
|
257
|
+
description: str | None = Field(alias="Description", default=None)
|
|
258
|
+
implements: ViewEntityList | None = Field(None, alias="Implements")
|
|
259
|
+
reference: URLEntity | ReferenceEntity | None = Field(alias="Reference", default=None, union_mode="left_to_right")
|
|
260
|
+
filter_: HasDataFilter | NodeTypeFilter | None = Field(None, alias="Filter")
|
|
261
|
+
in_model: bool = Field(True, alias="In Model")
|
|
262
|
+
class_: ClassEntity = Field(alias="Class (linage)")
|
|
263
|
+
|
|
264
|
+
def as_view(self) -> dm.ViewApply:
|
|
265
|
+
view_id = self.view.as_id()
|
|
266
|
+
return dm.ViewApply(
|
|
267
|
+
space=view_id.space,
|
|
268
|
+
external_id=view_id.external_id,
|
|
269
|
+
version=view_id.version or _DEFAULT_VERSION,
|
|
270
|
+
name=self.name or None,
|
|
271
|
+
description=self.description,
|
|
272
|
+
implements=[parent.as_id() for parent in self.implements or []] or None,
|
|
273
|
+
properties={},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def from_view(cls, view: dm.ViewApply, in_model: bool) -> "DMSView":
|
|
278
|
+
view_entity = ViewEntity.from_id(view.as_id())
|
|
279
|
+
class_entity = view_entity.as_class(skip_version=True)
|
|
280
|
+
|
|
281
|
+
return cls(
|
|
282
|
+
class_=class_entity,
|
|
283
|
+
view=view_entity,
|
|
284
|
+
description=view.description,
|
|
285
|
+
name=view.name,
|
|
286
|
+
implements=[ViewEntity.from_id(parent, _DEFAULT_VERSION) for parent in view.implements] or None,
|
|
287
|
+
in_model=in_model,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class DMSRules(BaseRules):
|
|
292
|
+
metadata: DMSMetadata = Field(alias="Metadata")
|
|
293
|
+
properties: SheetList[DMSProperty] = Field(alias="Properties")
|
|
294
|
+
views: SheetList[DMSView] = Field(alias="Views")
|
|
295
|
+
containers: SheetList[DMSContainer] | None = Field(None, alias="Containers")
|
|
296
|
+
last: "DMSRules | None" = Field(None, alias="Last", description="The previous version of the data model")
|
|
297
|
+
reference: "DMSRules | None" = Field(None, alias="Reference")
|
|
298
|
+
|
|
299
|
+
@field_validator("reference")
|
|
300
|
+
def check_reference_of_reference(cls, value: "DMSRules | None", info: ValidationInfo) -> "DMSRules | None":
|
|
301
|
+
if value is None:
|
|
302
|
+
return None
|
|
303
|
+
if value.reference is not None:
|
|
304
|
+
raise ValueError("Reference rules cannot have a reference")
|
|
305
|
+
if value.metadata.data_model_type == DataModelType.solution and (metadata := info.data.get("metadata")):
|
|
306
|
+
warnings.warn(
|
|
307
|
+
issues.dms.SolutionOnTopOfSolutionModelWarning(
|
|
308
|
+
metadata.as_data_model_id(), value.metadata.as_data_model_id()
|
|
309
|
+
),
|
|
310
|
+
stacklevel=2,
|
|
311
|
+
)
|
|
312
|
+
return value
|
|
313
|
+
|
|
314
|
+
@field_validator("views")
|
|
315
|
+
def matching_version_and_space(cls, value: SheetList[DMSView], info: ValidationInfo) -> SheetList[DMSView]:
|
|
316
|
+
if not (metadata := info.data.get("metadata")):
|
|
317
|
+
return value
|
|
318
|
+
model_version = metadata.version
|
|
319
|
+
if different_version := [view.view.as_id() for view in value if view.view.version != model_version]:
|
|
320
|
+
warnings.warn(issues.dms.ViewModelVersionNotMatchingWarning(different_version, model_version), stacklevel=2)
|
|
321
|
+
if different_space := [view.view.as_id() for view in value if view.view.space != metadata.space]:
|
|
322
|
+
warnings.warn(issues.dms.ViewModelSpaceNotMatchingWarning(different_space, metadata.space), stacklevel=2)
|
|
323
|
+
return value
|
|
324
|
+
|
|
325
|
+
@model_validator(mode="after")
|
|
326
|
+
def post_validation(self) -> "DMSRules":
|
|
327
|
+
from ._validation import DMSPostValidation
|
|
328
|
+
|
|
329
|
+
issue_list = DMSPostValidation(self).validate()
|
|
330
|
+
if issue_list.warnings:
|
|
331
|
+
issue_list.trigger_warnings()
|
|
332
|
+
if issue_list.has_errors:
|
|
333
|
+
raise MultiValueError([error for error in issue_list if isinstance(error, issues.NeatValidationError)])
|
|
334
|
+
return self
|
|
335
|
+
|
|
336
|
+
@model_serializer(mode="wrap", when_used="always")
|
|
337
|
+
def dms_rules_serialization(
|
|
338
|
+
self,
|
|
339
|
+
handler: Callable,
|
|
340
|
+
info: SerializationInfo,
|
|
341
|
+
) -> dict[str, Any]:
|
|
342
|
+
from ._serializer import _DMSRulesSerializer
|
|
343
|
+
|
|
344
|
+
dumped = cast(dict[str, Any], handler(self, info))
|
|
345
|
+
space, version = self.metadata.space, self.metadata.version
|
|
346
|
+
return _DMSRulesSerializer(info, space, version).clean(dumped)
|
|
347
|
+
|
|
348
|
+
def as_schema(
|
|
349
|
+
self, include_ref: bool = False, include_pipeline: bool = False, instance_space: str | None = None
|
|
350
|
+
) -> DMSSchema:
|
|
351
|
+
from ._exporter import _DMSExporter
|
|
352
|
+
|
|
353
|
+
return _DMSExporter(self, include_ref, include_pipeline, instance_space).to_schema()
|
|
354
|
+
|
|
355
|
+
def as_information_architect_rules(self) -> "InformationRules":
|
|
356
|
+
from ._converter import _DMSRulesConverter
|
|
357
|
+
|
|
358
|
+
return _DMSRulesConverter(self).as_information_architect_rules()
|
|
359
|
+
|
|
360
|
+
def as_domain_expert_rules(self) -> DomainRules:
|
|
361
|
+
from ._converter import _DMSRulesConverter
|
|
362
|
+
|
|
363
|
+
return _DMSRulesConverter(self).as_domain_rules()
|
|
364
|
+
|
|
365
|
+
def reference_self(self) -> Self:
|
|
366
|
+
new_rules = self.model_copy(deep=True)
|
|
367
|
+
for prop in new_rules.properties:
|
|
368
|
+
prop.reference = ReferenceEntity(
|
|
369
|
+
prefix=prop.view.prefix, suffix=prop.view.suffix, version=prop.view.version, property=prop.property_
|
|
370
|
+
)
|
|
371
|
+
view: DMSView
|
|
372
|
+
for view in new_rules.views:
|
|
373
|
+
view.reference = ReferenceEntity(
|
|
374
|
+
prefix=view.view.prefix, suffix=view.view.suffix, version=view.view.version
|
|
375
|
+
)
|
|
376
|
+
container: DMSContainer
|
|
377
|
+
for container in new_rules.containers or []:
|
|
378
|
+
container.reference = ReferenceEntity(prefix=container.container.prefix, suffix=container.container.suffix)
|
|
379
|
+
return new_rules
|
|
@@ -5,6 +5,7 @@ from typing import Any, Literal, cast, overload
|
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
|
+
from cognite.neat.rules.models._base import DataModelType, ExtensionCategory, SchemaCompleteness
|
|
8
9
|
from cognite.neat.rules.models.data_types import DataType
|
|
9
10
|
from cognite.neat.rules.models.entities import (
|
|
10
11
|
ClassEntity,
|
|
@@ -15,12 +16,11 @@ from cognite.neat.rules.models.entities import (
|
|
|
15
16
|
ViewPropertyEntity,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
|
-
from .
|
|
19
|
-
from ._dms_architect_rules import DMSContainer, DMSMetadata, DMSProperty, DMSRules, DMSView
|
|
19
|
+
from ._rules import DMSContainer, DMSMetadata, DMSProperty, DMSRules, DMSView
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@dataclass
|
|
23
|
-
class
|
|
23
|
+
class DMSMetadataInput:
|
|
24
24
|
schema_: Literal["complete", "partial", "extended"]
|
|
25
25
|
space: str
|
|
26
26
|
external_id: str
|
|
@@ -34,7 +34,7 @@ class DMSMetadataWrite:
|
|
|
34
34
|
updated: datetime | str | None = None
|
|
35
35
|
|
|
36
36
|
@classmethod
|
|
37
|
-
def load(cls, data: dict[str, Any] | None) -> "
|
|
37
|
+
def load(cls, data: dict[str, Any] | None) -> "DMSMetadataInput | None":
|
|
38
38
|
if data is None:
|
|
39
39
|
return None
|
|
40
40
|
_add_alias(data, DMSMetadata)
|
|
@@ -69,7 +69,7 @@ class DMSMetadataWrite:
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
@dataclass
|
|
72
|
-
class
|
|
72
|
+
class DMSPropertyInput:
|
|
73
73
|
view: str
|
|
74
74
|
view_property: str | None
|
|
75
75
|
value_type: str
|
|
@@ -93,16 +93,16 @@ class DMSPropertyWrite:
|
|
|
93
93
|
|
|
94
94
|
@classmethod
|
|
95
95
|
@overload
|
|
96
|
-
def load(cls, data: dict[str, Any]) -> "
|
|
96
|
+
def load(cls, data: dict[str, Any]) -> "DMSPropertyInput": ...
|
|
97
97
|
|
|
98
98
|
@classmethod
|
|
99
99
|
@overload
|
|
100
|
-
def load(cls, data: list[dict[str, Any]]) -> list["
|
|
100
|
+
def load(cls, data: list[dict[str, Any]]) -> list["DMSPropertyInput"]: ...
|
|
101
101
|
|
|
102
102
|
@classmethod
|
|
103
103
|
def load(
|
|
104
104
|
cls, data: dict[str, Any] | list[dict[str, Any]] | None
|
|
105
|
-
) -> "
|
|
105
|
+
) -> "DMSPropertyInput | list[DMSPropertyInput] | None":
|
|
106
106
|
if data is None:
|
|
107
107
|
return None
|
|
108
108
|
if isinstance(data, list) or (isinstance(data, dict) and isinstance(data.get("data"), list)):
|
|
@@ -168,7 +168,7 @@ class DMSPropertyWrite:
|
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
@dataclass
|
|
171
|
-
class
|
|
171
|
+
class DMSContainerInput:
|
|
172
172
|
container: str
|
|
173
173
|
class_: str | None = None
|
|
174
174
|
name: str | None = None
|
|
@@ -182,16 +182,16 @@ class DMSContainerWrite:
|
|
|
182
182
|
|
|
183
183
|
@classmethod
|
|
184
184
|
@overload
|
|
185
|
-
def load(cls, data: dict[str, Any]) -> "
|
|
185
|
+
def load(cls, data: dict[str, Any]) -> "DMSContainerInput": ...
|
|
186
186
|
|
|
187
187
|
@classmethod
|
|
188
188
|
@overload
|
|
189
|
-
def load(cls, data: list[dict[str, Any]]) -> list["
|
|
189
|
+
def load(cls, data: list[dict[str, Any]]) -> list["DMSContainerInput"]: ...
|
|
190
190
|
|
|
191
191
|
@classmethod
|
|
192
192
|
def load(
|
|
193
193
|
cls, data: dict[str, Any] | list[dict[str, Any]] | None
|
|
194
|
-
) -> "
|
|
194
|
+
) -> "DMSContainerInput | list[DMSContainerInput] | None":
|
|
195
195
|
if data is None:
|
|
196
196
|
return None
|
|
197
197
|
if isinstance(data, list) or (isinstance(data, dict) and isinstance(data.get("data"), list)):
|
|
@@ -230,7 +230,7 @@ class DMSContainerWrite:
|
|
|
230
230
|
|
|
231
231
|
|
|
232
232
|
@dataclass
|
|
233
|
-
class
|
|
233
|
+
class DMSViewInput:
|
|
234
234
|
view: str
|
|
235
235
|
class_: str | None = None
|
|
236
236
|
name: str | None = None
|
|
@@ -246,14 +246,14 @@ class DMSViewWrite:
|
|
|
246
246
|
|
|
247
247
|
@classmethod
|
|
248
248
|
@overload
|
|
249
|
-
def load(cls, data: dict[str, Any]) -> "
|
|
249
|
+
def load(cls, data: dict[str, Any]) -> "DMSViewInput": ...
|
|
250
250
|
|
|
251
251
|
@classmethod
|
|
252
252
|
@overload
|
|
253
|
-
def load(cls, data: list[dict[str, Any]]) -> list["
|
|
253
|
+
def load(cls, data: list[dict[str, Any]]) -> list["DMSViewInput"]: ...
|
|
254
254
|
|
|
255
255
|
@classmethod
|
|
256
|
-
def load(cls, data: dict[str, Any] | list[dict[str, Any]] | None) -> "
|
|
256
|
+
def load(cls, data: dict[str, Any] | list[dict[str, Any]] | None) -> "DMSViewInput | list[DMSViewInput] | None":
|
|
257
257
|
if data is None:
|
|
258
258
|
return None
|
|
259
259
|
if isinstance(data, list) or (isinstance(data, dict) and isinstance(data.get("data"), list)):
|
|
@@ -298,52 +298,61 @@ class DMSViewWrite:
|
|
|
298
298
|
|
|
299
299
|
|
|
300
300
|
@dataclass
|
|
301
|
-
class
|
|
302
|
-
metadata:
|
|
303
|
-
properties: Sequence[
|
|
304
|
-
views: Sequence[
|
|
305
|
-
containers: Sequence[
|
|
306
|
-
|
|
301
|
+
class DMSRulesInput:
|
|
302
|
+
metadata: DMSMetadataInput
|
|
303
|
+
properties: Sequence[DMSPropertyInput]
|
|
304
|
+
views: Sequence[DMSViewInput]
|
|
305
|
+
containers: Sequence[DMSContainerInput] | None = None
|
|
306
|
+
last: "DMSRulesInput | DMSRules | None" = None
|
|
307
|
+
reference: "DMSRulesInput | DMSRules | None" = None
|
|
307
308
|
|
|
308
309
|
@classmethod
|
|
309
310
|
@overload
|
|
310
|
-
def load(cls, data: dict[str, Any]) -> "
|
|
311
|
+
def load(cls, data: dict[str, Any]) -> "DMSRulesInput": ...
|
|
311
312
|
|
|
312
313
|
@classmethod
|
|
313
314
|
@overload
|
|
314
315
|
def load(cls, data: None) -> None: ...
|
|
315
316
|
|
|
316
317
|
@classmethod
|
|
317
|
-
def load(cls, data: dict | None) -> "
|
|
318
|
+
def load(cls, data: dict | None) -> "DMSRulesInput | None":
|
|
318
319
|
if data is None:
|
|
319
320
|
return None
|
|
320
321
|
_add_alias(data, DMSRules)
|
|
321
322
|
return cls(
|
|
322
|
-
metadata=
|
|
323
|
-
properties=
|
|
324
|
-
views=
|
|
325
|
-
containers=
|
|
326
|
-
|
|
323
|
+
metadata=DMSMetadataInput.load(data.get("metadata")), # type: ignore[arg-type]
|
|
324
|
+
properties=DMSPropertyInput.load(data.get("properties")), # type: ignore[arg-type]
|
|
325
|
+
views=DMSViewInput.load(data.get("views")), # type: ignore[arg-type]
|
|
326
|
+
containers=DMSContainerInput.load(data.get("containers")) or [],
|
|
327
|
+
last=DMSRulesInput.load(data.get("last")),
|
|
328
|
+
reference=DMSRulesInput.load(data.get("reference")),
|
|
327
329
|
)
|
|
328
330
|
|
|
329
|
-
def
|
|
331
|
+
def as_rules(self) -> DMSRules:
|
|
330
332
|
return DMSRules.model_validate(self.dump())
|
|
331
333
|
|
|
332
334
|
def dump(self) -> dict[str, Any]:
|
|
333
335
|
default_space = self.metadata.space
|
|
334
336
|
default_version = self.metadata.version
|
|
335
337
|
reference: dict[str, Any] | None = None
|
|
336
|
-
if isinstance(self.reference,
|
|
338
|
+
if isinstance(self.reference, DMSRulesInput):
|
|
337
339
|
reference = self.reference.dump()
|
|
338
340
|
elif isinstance(self.reference, DMSRules):
|
|
339
|
-
# We need to load through the
|
|
340
|
-
reference =
|
|
341
|
+
# We need to load through the DMSRulesInput to set the correct default space and version
|
|
342
|
+
reference = DMSRulesInput.load(self.reference.model_dump()).dump()
|
|
343
|
+
last: dict[str, Any] | None = None
|
|
344
|
+
if isinstance(self.last, DMSRulesInput):
|
|
345
|
+
last = self.last.dump()
|
|
346
|
+
elif isinstance(self.last, DMSRules):
|
|
347
|
+
# We need to load through the DMSRulesInput to set the correct default space and version
|
|
348
|
+
last = DMSRulesInput.load(self.last.model_dump()).dump()
|
|
341
349
|
|
|
342
350
|
return dict(
|
|
343
351
|
Metadata=self.metadata.dump(),
|
|
344
352
|
Properties=[prop.dump(default_space, default_version) for prop in self.properties],
|
|
345
353
|
Views=[view.dump(default_space, default_version) for view in self.views],
|
|
346
354
|
Containers=[container.dump(default_space) for container in self.containers or []] or None,
|
|
355
|
+
Last=last,
|
|
347
356
|
Reference=reference,
|
|
348
357
|
)
|
|
349
358
|
|
|
@@ -20,6 +20,7 @@ from cognite.neat.rules.issues.dms import (
|
|
|
20
20
|
DirectRelationMissingSourceWarning,
|
|
21
21
|
DMSSchemaError,
|
|
22
22
|
DuplicatedViewInDataModelError,
|
|
23
|
+
IncompleteSchemaError,
|
|
23
24
|
MissingContainerError,
|
|
24
25
|
MissingContainerPropertyError,
|
|
25
26
|
MissingEdgeViewError,
|
|
@@ -32,6 +33,7 @@ from cognite.neat.rules.models.data_types import _DATA_TYPE_BY_DMS_TYPE
|
|
|
32
33
|
from cognite.neat.utils.cdf_loaders import ViewLoader
|
|
33
34
|
from cognite.neat.utils.cdf_loaders.data_classes import RawTableWrite, RawTableWriteList
|
|
34
35
|
from cognite.neat.utils.text import to_camel
|
|
36
|
+
from cognite.neat.utils.utils import get_inheritance_path
|
|
35
37
|
|
|
36
38
|
if sys.version_info >= (3, 11):
|
|
37
39
|
from typing import Self
|
|
@@ -41,14 +43,16 @@ else:
|
|
|
41
43
|
|
|
42
44
|
@dataclass
|
|
43
45
|
class DMSSchema:
|
|
44
|
-
spaces: dm.SpaceApplyList = field(default_factory=lambda: dm.SpaceApplyList([]))
|
|
45
46
|
data_models: dm.DataModelApplyList = field(default_factory=lambda: dm.DataModelApplyList([]))
|
|
47
|
+
spaces: dm.SpaceApplyList = field(default_factory=lambda: dm.SpaceApplyList([]))
|
|
46
48
|
views: dm.ViewApplyList = field(default_factory=lambda: dm.ViewApplyList([]))
|
|
47
49
|
containers: dm.ContainerApplyList = field(default_factory=lambda: dm.ContainerApplyList([]))
|
|
48
50
|
node_types: dm.NodeApplyList = field(default_factory=lambda: dm.NodeApplyList([]))
|
|
49
|
-
# The
|
|
50
|
-
#
|
|
51
|
-
|
|
51
|
+
# The last schema is the previous version of the data model. In the case, extension=additio, this
|
|
52
|
+
# should not be modified.
|
|
53
|
+
last: "DMSSchema | None" = None
|
|
54
|
+
# Reference is typically the Enterprise model, while this is the solution model.
|
|
55
|
+
reference: "DMSSchema | None" = None
|
|
52
56
|
|
|
53
57
|
_FIELD_NAME_BY_RESOURCE_TYPE: ClassVar[dict[str, str]] = {
|
|
54
58
|
"container": "containers",
|
|
@@ -58,6 +62,30 @@ class DMSSchema:
|
|
|
58
62
|
"node": "node_types",
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
def _get_mapped_container_from_view(self, view_id: dm.ViewId) -> set[dm.ContainerId]:
|
|
66
|
+
# index all views, including ones from reference
|
|
67
|
+
indexed_views = {
|
|
68
|
+
**{view.as_id(): view for view in self.views},
|
|
69
|
+
**({view.as_id(): view for view in self.reference.views} if self.reference else {}),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if view_id not in indexed_views:
|
|
73
|
+
raise ValueError(f"View {view_id} not found")
|
|
74
|
+
|
|
75
|
+
indexed_implemented_views = {id_: view.implements for id_, view in indexed_views.items()}
|
|
76
|
+
view_inheritance = get_inheritance_path(view_id, indexed_implemented_views)
|
|
77
|
+
|
|
78
|
+
directly_referenced_containers = indexed_views[view_id].referenced_containers()
|
|
79
|
+
inherited_referenced_containers = set()
|
|
80
|
+
|
|
81
|
+
for view_id in view_inheritance:
|
|
82
|
+
if implemented_view := indexed_views.get(view_id):
|
|
83
|
+
inherited_referenced_containers |= implemented_view.referenced_containers()
|
|
84
|
+
else:
|
|
85
|
+
raise IncompleteSchemaError(missing_component=view_id).as_exception()
|
|
86
|
+
|
|
87
|
+
return directly_referenced_containers | inherited_referenced_containers
|
|
88
|
+
|
|
61
89
|
@classmethod
|
|
62
90
|
def from_model_id(cls, client: CogniteClient, data_model_id: dm.DataModelIdentifier) -> "DMSSchema":
|
|
63
91
|
data_models = client.data_modeling.data_models.retrieve(data_model_id, inline_views=True)
|
|
@@ -351,6 +379,10 @@ class DMSSchema:
|
|
|
351
379
|
defined_spaces = {space.space for space in self.spaces}
|
|
352
380
|
defined_containers = {container.as_id(): container for container in self.containers}
|
|
353
381
|
defined_views = {view.as_id() for view in self.views}
|
|
382
|
+
if self.reference:
|
|
383
|
+
defined_spaces |= {space.space for space in self.reference.spaces}
|
|
384
|
+
defined_containers |= {container.as_id(): container for container in self.reference.containers}
|
|
385
|
+
defined_views |= {view.as_id() for view in self.reference.views}
|
|
354
386
|
|
|
355
387
|
for container in self.containers:
|
|
356
388
|
if container.space not in defined_spaces:
|