cognite-neat 0.76.0__py3-none-any.whl → 0.76.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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} +65 -57
- 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.0.dist-info → cognite_neat-0.76.2.dist-info}/METADATA +1 -1
- {cognite_neat-0.76.0.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.0.dist-info → cognite_neat-0.76.2.dist-info}/LICENSE +0 -0
- {cognite_neat-0.76.0.dist-info → cognite_neat-0.76.2.dist-info}/WHEEL +0 -0
- {cognite_neat-0.76.0.dist-info → cognite_neat-0.76.2.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)):
|
|
@@ -146,9 +146,9 @@ class DMSPropertyWrite:
|
|
|
146
146
|
"View Property": self.view_property,
|
|
147
147
|
"Value Type": value_type,
|
|
148
148
|
"Property (linage)": self.property_ or self.view_property,
|
|
149
|
-
"Class (linage)":
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
"Class (linage)": (
|
|
150
|
+
ClassEntity.load(self.class_, prefix=default_space, version=default_version) if self.class_ else None
|
|
151
|
+
),
|
|
152
152
|
"Name": self.name,
|
|
153
153
|
"Description": self.description,
|
|
154
154
|
"Connection": self.connection,
|
|
@@ -156,9 +156,11 @@ class DMSPropertyWrite:
|
|
|
156
156
|
"Is List": self.is_list,
|
|
157
157
|
"Default": self.default,
|
|
158
158
|
"Reference": self.reference,
|
|
159
|
-
"Container":
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
"Container": (
|
|
160
|
+
ContainerEntity.load(self.container, space=default_space, version=default_version)
|
|
161
|
+
if self.container
|
|
162
|
+
else None
|
|
163
|
+
),
|
|
162
164
|
"Container Property": self.container_property,
|
|
163
165
|
"Index": self.index,
|
|
164
166
|
"Constraint": self.constraint,
|
|
@@ -166,7 +168,7 @@ class DMSPropertyWrite:
|
|
|
166
168
|
|
|
167
169
|
|
|
168
170
|
@dataclass
|
|
169
|
-
class
|
|
171
|
+
class DMSContainerInput:
|
|
170
172
|
container: str
|
|
171
173
|
class_: str | None = None
|
|
172
174
|
name: str | None = None
|
|
@@ -180,16 +182,16 @@ class DMSContainerWrite:
|
|
|
180
182
|
|
|
181
183
|
@classmethod
|
|
182
184
|
@overload
|
|
183
|
-
def load(cls, data: dict[str, Any]) -> "
|
|
185
|
+
def load(cls, data: dict[str, Any]) -> "DMSContainerInput": ...
|
|
184
186
|
|
|
185
187
|
@classmethod
|
|
186
188
|
@overload
|
|
187
|
-
def load(cls, data: list[dict[str, Any]]) -> list["
|
|
189
|
+
def load(cls, data: list[dict[str, Any]]) -> list["DMSContainerInput"]: ...
|
|
188
190
|
|
|
189
191
|
@classmethod
|
|
190
192
|
def load(
|
|
191
193
|
cls, data: dict[str, Any] | list[dict[str, Any]] | None
|
|
192
|
-
) -> "
|
|
194
|
+
) -> "DMSContainerInput | list[DMSContainerInput] | None":
|
|
193
195
|
if data is None:
|
|
194
196
|
return None
|
|
195
197
|
if isinstance(data, list) or (isinstance(data, dict) and isinstance(data.get("data"), list)):
|
|
@@ -210,23 +212,25 @@ class DMSContainerWrite:
|
|
|
210
212
|
container = ContainerEntity.load(self.container, space=default_space)
|
|
211
213
|
return {
|
|
212
214
|
"Container": container,
|
|
213
|
-
"Class (linage)":
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
"Class (linage)": (
|
|
216
|
+
ClassEntity.load(self.class_, prefix=default_space) if self.class_ else container.as_class()
|
|
217
|
+
),
|
|
216
218
|
"Name": self.name,
|
|
217
219
|
"Description": self.description,
|
|
218
220
|
"Reference": self.reference,
|
|
219
|
-
"Constraint":
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
"Constraint": (
|
|
222
|
+
[
|
|
223
|
+
ContainerEntity.load(constraint.strip(), space=default_space)
|
|
224
|
+
for constraint in self.constraint.split(",")
|
|
225
|
+
]
|
|
226
|
+
if self.constraint
|
|
227
|
+
else None
|
|
228
|
+
),
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
|
|
228
232
|
@dataclass
|
|
229
|
-
class
|
|
233
|
+
class DMSViewInput:
|
|
230
234
|
view: str
|
|
231
235
|
class_: str | None = None
|
|
232
236
|
name: str | None = None
|
|
@@ -242,14 +246,14 @@ class DMSViewWrite:
|
|
|
242
246
|
|
|
243
247
|
@classmethod
|
|
244
248
|
@overload
|
|
245
|
-
def load(cls, data: dict[str, Any]) -> "
|
|
249
|
+
def load(cls, data: dict[str, Any]) -> "DMSViewInput": ...
|
|
246
250
|
|
|
247
251
|
@classmethod
|
|
248
252
|
@overload
|
|
249
|
-
def load(cls, data: list[dict[str, Any]]) -> list["
|
|
253
|
+
def load(cls, data: list[dict[str, Any]]) -> list["DMSViewInput"]: ...
|
|
250
254
|
|
|
251
255
|
@classmethod
|
|
252
|
-
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":
|
|
253
257
|
if data is None:
|
|
254
258
|
return None
|
|
255
259
|
if isinstance(data, list) or (isinstance(data, dict) and isinstance(data.get("data"), list)):
|
|
@@ -272,17 +276,21 @@ class DMSViewWrite:
|
|
|
272
276
|
view = ViewEntity.load(self.view, space=default_space, version=default_version)
|
|
273
277
|
return {
|
|
274
278
|
"View": view,
|
|
275
|
-
"Class (linage)":
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
"Class (linage)": (
|
|
280
|
+
ClassEntity.load(self.class_, prefix=default_space, version=default_version)
|
|
281
|
+
if self.class_
|
|
282
|
+
else view.as_class()
|
|
283
|
+
),
|
|
278
284
|
"Name": self.name,
|
|
279
285
|
"Description": self.description,
|
|
280
|
-
"Implements":
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
+
"Implements": (
|
|
287
|
+
[
|
|
288
|
+
ViewEntity.load(implement, space=default_space, version=default_version)
|
|
289
|
+
for implement in self.implements.split(",")
|
|
290
|
+
]
|
|
291
|
+
if self.implements
|
|
292
|
+
else None
|
|
293
|
+
),
|
|
286
294
|
"Reference": self.reference,
|
|
287
295
|
"Filter": self.filter_,
|
|
288
296
|
"In Model": self.in_model,
|
|
@@ -290,46 +298,46 @@ class DMSViewWrite:
|
|
|
290
298
|
|
|
291
299
|
|
|
292
300
|
@dataclass
|
|
293
|
-
class
|
|
294
|
-
metadata:
|
|
295
|
-
properties: Sequence[
|
|
296
|
-
views: Sequence[
|
|
297
|
-
containers: Sequence[
|
|
298
|
-
reference: "
|
|
301
|
+
class DMSRulesInput:
|
|
302
|
+
metadata: DMSMetadataInput
|
|
303
|
+
properties: Sequence[DMSPropertyInput]
|
|
304
|
+
views: Sequence[DMSViewInput]
|
|
305
|
+
containers: Sequence[DMSContainerInput] | None = None
|
|
306
|
+
reference: "DMSRulesInput | DMSRules | None" = None
|
|
299
307
|
|
|
300
308
|
@classmethod
|
|
301
309
|
@overload
|
|
302
|
-
def load(cls, data: dict[str, Any]) -> "
|
|
310
|
+
def load(cls, data: dict[str, Any]) -> "DMSRulesInput": ...
|
|
303
311
|
|
|
304
312
|
@classmethod
|
|
305
313
|
@overload
|
|
306
314
|
def load(cls, data: None) -> None: ...
|
|
307
315
|
|
|
308
316
|
@classmethod
|
|
309
|
-
def load(cls, data: dict | None) -> "
|
|
317
|
+
def load(cls, data: dict | None) -> "DMSRulesInput | None":
|
|
310
318
|
if data is None:
|
|
311
319
|
return None
|
|
312
320
|
_add_alias(data, DMSRules)
|
|
313
321
|
return cls(
|
|
314
|
-
metadata=
|
|
315
|
-
properties=
|
|
316
|
-
views=
|
|
317
|
-
containers=
|
|
318
|
-
reference=
|
|
322
|
+
metadata=DMSMetadataInput.load(data.get("metadata")), # type: ignore[arg-type]
|
|
323
|
+
properties=DMSPropertyInput.load(data.get("properties")), # type: ignore[arg-type]
|
|
324
|
+
views=DMSViewInput.load(data.get("views")), # type: ignore[arg-type]
|
|
325
|
+
containers=DMSContainerInput.load(data.get("containers")) or [],
|
|
326
|
+
reference=DMSRulesInput.load(data.get("reference")),
|
|
319
327
|
)
|
|
320
328
|
|
|
321
|
-
def
|
|
329
|
+
def as_rules(self) -> DMSRules:
|
|
322
330
|
return DMSRules.model_validate(self.dump())
|
|
323
331
|
|
|
324
332
|
def dump(self) -> dict[str, Any]:
|
|
325
333
|
default_space = self.metadata.space
|
|
326
334
|
default_version = self.metadata.version
|
|
327
335
|
reference: dict[str, Any] | None = None
|
|
328
|
-
if isinstance(self.reference,
|
|
336
|
+
if isinstance(self.reference, DMSRulesInput):
|
|
329
337
|
reference = self.reference.dump()
|
|
330
338
|
elif isinstance(self.reference, DMSRules):
|
|
331
|
-
# We need to load through the
|
|
332
|
-
reference =
|
|
339
|
+
# We need to load through the DMSRulesInput to set the correct default space and version
|
|
340
|
+
reference = DMSRulesInput.load(self.reference.model_dump()).dump()
|
|
333
341
|
|
|
334
342
|
return dict(
|
|
335
343
|
Metadata=self.metadata.dump(),
|
|
@@ -41,14 +41,16 @@ else:
|
|
|
41
41
|
|
|
42
42
|
@dataclass
|
|
43
43
|
class DMSSchema:
|
|
44
|
-
spaces: dm.SpaceApplyList = field(default_factory=lambda: dm.SpaceApplyList([]))
|
|
45
44
|
data_models: dm.DataModelApplyList = field(default_factory=lambda: dm.DataModelApplyList([]))
|
|
45
|
+
spaces: dm.SpaceApplyList = field(default_factory=lambda: dm.SpaceApplyList([]))
|
|
46
46
|
views: dm.ViewApplyList = field(default_factory=lambda: dm.ViewApplyList([]))
|
|
47
47
|
containers: dm.ContainerApplyList = field(default_factory=lambda: dm.ContainerApplyList([]))
|
|
48
48
|
node_types: dm.NodeApplyList = field(default_factory=lambda: dm.NodeApplyList([]))
|
|
49
|
-
# The
|
|
50
|
-
#
|
|
51
|
-
|
|
49
|
+
# The last schema is the previous version of the data model. In the case, extension=additio, this
|
|
50
|
+
# should not be modified.
|
|
51
|
+
last: "DMSSchema | None" = None
|
|
52
|
+
# Reference is typically the Enterprise model, while this is the solution model.
|
|
53
|
+
reference: "DMSSchema | None" = None
|
|
52
54
|
|
|
53
55
|
_FIELD_NAME_BY_RESOURCE_TYPE: ClassVar[dict[str, str]] = {
|
|
54
56
|
"container": "containers",
|
|
@@ -351,6 +353,10 @@ class DMSSchema:
|
|
|
351
353
|
defined_spaces = {space.space for space in self.spaces}
|
|
352
354
|
defined_containers = {container.as_id(): container for container in self.containers}
|
|
353
355
|
defined_views = {view.as_id() for view in self.views}
|
|
356
|
+
if self.reference:
|
|
357
|
+
defined_spaces |= {space.space for space in self.reference.spaces}
|
|
358
|
+
defined_containers |= {container.as_id(): container for container in self.reference.containers}
|
|
359
|
+
defined_views |= {view.as_id() for view in self.reference.views}
|
|
354
360
|
|
|
355
361
|
for container in self.containers:
|
|
356
362
|
if container.space not in defined_spaces:
|