cognite-neat 0.75.8__py3-none-any.whl → 0.76.0__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/configuration.py +4 -9
- cognite/neat/app/api/routers/configuration.py +2 -1
- cognite/neat/app/api/routers/crud.py +5 -5
- cognite/neat/app/api/routers/data_exploration.py +3 -1
- cognite/neat/app/api/routers/rules.py +3 -3
- cognite/neat/app/api/routers/workflows.py +3 -3
- cognite/neat/app/ui/neat-app/build/asset-manifest.json +3 -3
- cognite/neat/app/ui/neat-app/build/index.html +1 -1
- cognite/neat/app/ui/neat-app/build/static/js/{main.4345d42f.js → main.ec7f72e2.js} +3 -3
- cognite/neat/app/ui/neat-app/build/static/js/{main.4345d42f.js.map → main.ec7f72e2.js.map} +1 -1
- cognite/neat/config.py +147 -12
- cognite/neat/constants.py +1 -0
- cognite/neat/graph/exceptions.py +1 -2
- cognite/neat/legacy/graph/exceptions.py +1 -2
- cognite/neat/legacy/graph/extractors/_mock_graph_generator.py +1 -2
- cognite/neat/legacy/graph/loaders/_asset_loader.py +8 -13
- cognite/neat/legacy/graph/loaders/_base.py +2 -4
- cognite/neat/legacy/graph/loaders/_exceptions.py +1 -3
- cognite/neat/legacy/graph/loaders/core/rdf_to_assets.py +4 -8
- cognite/neat/legacy/graph/loaders/core/rdf_to_relationships.py +2 -4
- cognite/neat/legacy/graph/loaders/rdf_to_dms.py +2 -4
- cognite/neat/legacy/graph/loaders/validator.py +1 -1
- cognite/neat/legacy/graph/transformations/transformer.py +1 -2
- cognite/neat/legacy/rules/exporters/_rules2dms.py +1 -2
- cognite/neat/legacy/rules/exporters/_validation.py +4 -8
- cognite/neat/legacy/rules/importers/_base.py +0 -4
- cognite/neat/legacy/rules/importers/_dms2rules.py +0 -2
- cognite/neat/legacy/rules/models/rdfpath.py +1 -2
- cognite/neat/legacy/workflows/examples/Export_DMS/workflow.yaml +89 -0
- cognite/neat/legacy/workflows/examples/Export_Rules_to_Ontology/workflow.yaml +152 -0
- cognite/neat/legacy/workflows/examples/Extract_DEXPI_Graph_and_Export_Rules/workflow.yaml +139 -0
- cognite/neat/legacy/workflows/examples/Extract_RDF_Graph_and_Generate_Assets/workflow.yaml +270 -0
- cognite/neat/legacy/workflows/examples/Import_DMS/workflow.yaml +65 -0
- cognite/neat/legacy/workflows/examples/Ontology_to_Data_Model/workflow.yaml +116 -0
- cognite/neat/legacy/workflows/examples/Validate_Rules/workflow.yaml +67 -0
- cognite/neat/legacy/workflows/examples/Validate_Solution_Model/workflow.yaml +64 -0
- cognite/neat/legacy/workflows/examples/Visualize_Data_Model_Using_Mock_Graph/workflow.yaml +95 -0
- cognite/neat/legacy/workflows/examples/Visualize_Semantic_Data_Model/workflow.yaml +111 -0
- cognite/neat/rules/exporters/_models.py +3 -0
- cognite/neat/rules/exporters/_rules2dms.py +46 -4
- cognite/neat/rules/exporters/_rules2excel.py +2 -11
- cognite/neat/rules/exporters/_validation.py +6 -8
- cognite/neat/rules/importers/_base.py +8 -4
- cognite/neat/rules/importers/_dms2rules.py +321 -129
- cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +2 -4
- cognite/neat/rules/importers/_dtdl2rules/spec.py +2 -4
- cognite/neat/rules/importers/_owl2rules/_owl2rules.py +2 -4
- cognite/neat/rules/importers/_spreadsheet2rules.py +18 -16
- cognite/neat/rules/importers/_yaml2rules.py +2 -4
- cognite/neat/rules/issues/base.py +3 -0
- cognite/neat/rules/issues/dms.py +144 -58
- cognite/neat/rules/issues/fileread.py +41 -0
- cognite/neat/rules/issues/formatters.py +3 -1
- cognite/neat/rules/issues/importing.py +155 -0
- cognite/neat/rules/issues/spreadsheet.py +12 -9
- cognite/neat/rules/models/entities.py +30 -8
- cognite/neat/rules/models/rdfpath.py +1 -2
- cognite/neat/rules/models/rules/_base.py +5 -6
- cognite/neat/rules/models/rules/_dms_architect_rules.py +494 -333
- cognite/neat/rules/models/rules/_dms_rules_write.py +43 -52
- cognite/neat/rules/models/rules/_dms_schema.py +112 -22
- cognite/neat/rules/models/rules/_domain_rules.py +5 -0
- cognite/neat/rules/models/rules/_information_rules.py +13 -6
- cognite/neat/rules/models/wrapped_entities.py +166 -0
- cognite/neat/utils/cdf_loaders/_data_modeling.py +3 -1
- cognite/neat/utils/cdf_loaders/_ingestion.py +2 -4
- cognite/neat/utils/spreadsheet.py +2 -4
- cognite/neat/utils/utils.py +2 -4
- cognite/neat/workflows/base.py +5 -5
- cognite/neat/workflows/manager.py +32 -22
- cognite/neat/workflows/model.py +3 -3
- cognite/neat/workflows/steps/lib/__init__.py +0 -7
- cognite/neat/workflows/steps/lib/current/__init__.py +6 -0
- cognite/neat/workflows/steps/lib/{rules_exporter.py → current/rules_exporter.py} +8 -8
- cognite/neat/workflows/steps/lib/{rules_importer.py → current/rules_importer.py} +4 -4
- cognite/neat/workflows/steps/lib/io/__init__.py +1 -0
- cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_contextualization.py +2 -2
- cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_extractor.py +9 -9
- cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_loader.py +9 -9
- cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_store.py +4 -4
- cognite/neat/workflows/steps/lib/{v1 → legacy}/graph_transformer.py +2 -2
- cognite/neat/workflows/steps/lib/{v1 → legacy}/rules_exporter.py +15 -17
- cognite/neat/workflows/steps/lib/{v1 → legacy}/rules_importer.py +7 -7
- cognite/neat/workflows/steps/step_model.py +5 -9
- cognite/neat/workflows/steps_registry.py +20 -11
- {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/RECORD +98 -86
- cognite/neat/app/api/data_classes/configuration.py +0 -121
- /cognite/neat/app/ui/neat-app/build/static/js/{main.4345d42f.js.LICENSE.txt → main.ec7f72e2.js.LICENSE.txt} +0 -0
- /cognite/neat/workflows/steps/lib/{graph_extractor.py → current/graph_extractor.py} +0 -0
- /cognite/neat/workflows/steps/lib/{graph_loader.py → current/graph_loader.py} +0 -0
- /cognite/neat/workflows/steps/lib/{graph_store.py → current/graph_store.py} +0 -0
- /cognite/neat/workflows/steps/lib/{rules_validator.py → current/rules_validator.py} +0 -0
- /cognite/neat/workflows/steps/lib/{io_steps.py → io/io_steps.py} +0 -0
- /cognite/neat/workflows/steps/lib/{v1 → legacy}/__init__.py +0 -0
- {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.75.8.dist-info → cognite_neat-0.76.0.dist-info}/entry_points.txt +0 -0
|
@@ -4,13 +4,18 @@ import re
|
|
|
4
4
|
import sys
|
|
5
5
|
import warnings
|
|
6
6
|
from collections import defaultdict
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from datetime import datetime
|
|
8
9
|
from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast
|
|
9
10
|
|
|
10
11
|
from cognite.client import data_modeling as dm
|
|
11
12
|
from cognite.client.data_classes.data_modeling import PropertyType as CognitePropertyType
|
|
12
13
|
from cognite.client.data_classes.data_modeling.containers import BTreeIndex
|
|
13
|
-
from cognite.client.data_classes.data_modeling.views import
|
|
14
|
+
from cognite.client.data_classes.data_modeling.views import (
|
|
15
|
+
SingleEdgeConnectionApply,
|
|
16
|
+
SingleReverseDirectRelationApply,
|
|
17
|
+
ViewPropertyApply,
|
|
18
|
+
)
|
|
14
19
|
from pydantic import Field, field_serializer, field_validator, model_serializer, model_validator
|
|
15
20
|
from pydantic_core.core_schema import SerializationInfo, ValidationInfo
|
|
16
21
|
from rdflib import Namespace
|
|
@@ -22,6 +27,7 @@ from cognite.neat.rules.models.entities import (
|
|
|
22
27
|
ClassEntity,
|
|
23
28
|
ContainerEntity,
|
|
24
29
|
ContainerEntityList,
|
|
30
|
+
DMSNodeEntity,
|
|
25
31
|
DMSUnknownEntity,
|
|
26
32
|
ParentClassEntity,
|
|
27
33
|
ReferenceEntity,
|
|
@@ -32,8 +38,18 @@ from cognite.neat.rules.models.entities import (
|
|
|
32
38
|
ViewPropertyEntity,
|
|
33
39
|
)
|
|
34
40
|
from cognite.neat.rules.models.rules._domain_rules import DomainRules
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
from cognite.neat.rules.models.wrapped_entities import DMSFilter, HasDataFilter, NodeTypeFilter
|
|
42
|
+
|
|
43
|
+
from ._base import (
|
|
44
|
+
BaseMetadata,
|
|
45
|
+
BaseRules,
|
|
46
|
+
DataModelType,
|
|
47
|
+
ExtensionCategory,
|
|
48
|
+
RoleTypes,
|
|
49
|
+
SchemaCompleteness,
|
|
50
|
+
SheetEntity,
|
|
51
|
+
SheetList,
|
|
52
|
+
)
|
|
37
53
|
from ._dms_schema import DMSSchema, PipelineSchema
|
|
38
54
|
from ._types import (
|
|
39
55
|
ExternalIdType,
|
|
@@ -67,6 +83,7 @@ del subclasses # cleanup namespace
|
|
|
67
83
|
|
|
68
84
|
class DMSMetadata(BaseMetadata):
|
|
69
85
|
role: ClassVar[RoleTypes] = RoleTypes.dms_architect
|
|
86
|
+
data_model_type: DataModelType = Field(DataModelType.solution, alias="dataModelType")
|
|
70
87
|
schema_: SchemaCompleteness = Field(alias="schema")
|
|
71
88
|
extension: ExtensionCategory = ExtensionCategory.addition
|
|
72
89
|
space: ExternalIdType
|
|
@@ -86,8 +103,6 @@ class DMSMetadata(BaseMetadata):
|
|
|
86
103
|
updated: datetime = Field(
|
|
87
104
|
description=("Date of the data model update"),
|
|
88
105
|
)
|
|
89
|
-
# MyPy does not account for the field validator below that sets the default value
|
|
90
|
-
default_view_version: VersionType = Field(None) # type: ignore[assignment]
|
|
91
106
|
|
|
92
107
|
@field_validator("*", mode="before")
|
|
93
108
|
def strip_string(cls, value: Any) -> Any:
|
|
@@ -95,15 +110,22 @@ class DMSMetadata(BaseMetadata):
|
|
|
95
110
|
return value.strip()
|
|
96
111
|
return value
|
|
97
112
|
|
|
113
|
+
@field_serializer("schema_", "extension", "data_model_type", when_used="always")
|
|
114
|
+
@staticmethod
|
|
115
|
+
def as_string(value: SchemaCompleteness | ExtensionCategory | DataModelType) -> str:
|
|
116
|
+
return str(value)
|
|
117
|
+
|
|
98
118
|
@field_validator("schema_", mode="plain")
|
|
99
|
-
def
|
|
119
|
+
def as_enum_schema(cls, value: str) -> SchemaCompleteness:
|
|
100
120
|
return SchemaCompleteness(value)
|
|
101
121
|
|
|
102
|
-
@
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
@field_validator("extension", mode="plain")
|
|
123
|
+
def as_enum_extension(cls, value: str) -> ExtensionCategory:
|
|
124
|
+
return ExtensionCategory(value)
|
|
125
|
+
|
|
126
|
+
@field_validator("data_model_type", mode="plain")
|
|
127
|
+
def as_enum_model_type(cls, value: str) -> DataModelType:
|
|
128
|
+
return DataModelType(value)
|
|
107
129
|
|
|
108
130
|
@field_validator("description", mode="before")
|
|
109
131
|
def nan_as_none(cls, value):
|
|
@@ -116,6 +138,9 @@ class DMSMetadata(BaseMetadata):
|
|
|
116
138
|
space=self.space,
|
|
117
139
|
)
|
|
118
140
|
|
|
141
|
+
def as_data_model_id(self) -> dm.DataModelId:
|
|
142
|
+
return dm.DataModelId(space=self.space, external_id=self.external_id, version=self.version)
|
|
143
|
+
|
|
119
144
|
def as_data_model(self) -> dm.DataModelApply:
|
|
120
145
|
suffix = f"Creator: {', '.join(self.creator)}"
|
|
121
146
|
if self.description:
|
|
@@ -162,44 +187,42 @@ class DMSMetadata(BaseMetadata):
|
|
|
162
187
|
|
|
163
188
|
|
|
164
189
|
class DMSProperty(SheetEntity):
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
view: ViewEntity = Field(alias="View")
|
|
191
|
+
view_property: str = Field(alias="View Property")
|
|
192
|
+
name: str | None = Field(alias="Name", default=None)
|
|
193
|
+
description: str | None = Field(alias="Description", default=None)
|
|
194
|
+
connection: Literal["direct", "edge", "reverse"] | None = Field(None, alias="Connection")
|
|
167
195
|
value_type: DataType | ViewPropertyEntity | ViewEntity | DMSUnknownEntity = Field(alias="Value Type")
|
|
168
196
|
nullable: bool | None = Field(default=None, alias="Nullable")
|
|
169
|
-
is_list: bool | None = Field(default=None, alias="
|
|
197
|
+
is_list: bool | None = Field(default=None, alias="Is List")
|
|
170
198
|
default: str | int | dict | None = Field(None, alias="Default")
|
|
171
199
|
reference: URLEntity | ReferenceEntity | None = Field(default=None, alias="Reference", union_mode="left_to_right")
|
|
172
200
|
container: ContainerEntity | None = Field(None, alias="Container")
|
|
173
|
-
container_property: str | None = Field(None, alias="
|
|
174
|
-
view: ViewEntity = Field(alias="View")
|
|
175
|
-
view_property: str = Field(alias="ViewProperty")
|
|
201
|
+
container_property: str | None = Field(None, alias="Container Property")
|
|
176
202
|
index: StrListType | None = Field(None, alias="Index")
|
|
177
203
|
constraint: StrListType | None = Field(None, alias="Constraint")
|
|
204
|
+
class_: ClassEntity = Field(alias="Class (linage)")
|
|
205
|
+
property_: PropertyType = Field(alias="Property (linage)")
|
|
178
206
|
|
|
179
207
|
@field_validator("nullable")
|
|
180
208
|
def direct_relation_must_be_nullable(cls, value: Any, info: ValidationInfo) -> None:
|
|
181
|
-
if info.data.get("
|
|
209
|
+
if info.data.get("connection") == "direct" and value is False:
|
|
182
210
|
raise ValueError("Direct relation must be nullable")
|
|
183
211
|
return value
|
|
184
212
|
|
|
185
|
-
@field_validator("container_property", "container")
|
|
186
|
-
def reverse_direct_relation_has_no_container(cls, value, info: ValidationInfo) -> None:
|
|
187
|
-
if info.data.get("relation") == "reversedirect" and value is not None:
|
|
188
|
-
raise ValueError(f"Reverse direct relation must not have container or container property, got {value}")
|
|
189
|
-
return value
|
|
190
|
-
|
|
191
213
|
@field_validator("value_type", mode="after")
|
|
192
|
-
def
|
|
193
|
-
|
|
214
|
+
def connections_value_type(
|
|
215
|
+
cls, value: ViewPropertyEntity | ViewEntity | DMSUnknownEntity, info: ValidationInfo
|
|
216
|
+
) -> DataType | ViewPropertyEntity | ViewEntity | DMSUnknownEntity:
|
|
217
|
+
if (connection := info.data.get("connection")) is None:
|
|
194
218
|
return value
|
|
195
|
-
if not isinstance(value, ViewEntity |
|
|
196
|
-
raise ValueError(f"
|
|
197
|
-
|
|
198
|
-
|
|
219
|
+
if connection == "direct" and not isinstance(value, ViewEntity | DMSUnknownEntity):
|
|
220
|
+
raise ValueError(f"Direct relation must have a value type that points to a view, got {value}")
|
|
221
|
+
elif connection == "edge" and not isinstance(value, ViewEntity):
|
|
222
|
+
raise ValueError(f"Edge connection must have a value type that points to a view, got {value}")
|
|
223
|
+
elif connection == "reverse" and not isinstance(value, ViewPropertyEntity | ViewEntity):
|
|
199
224
|
raise ValueError(
|
|
200
|
-
"Reverse
|
|
201
|
-
f"Which property in {value.versioned_id} is this the reverse of? Expecting"
|
|
202
|
-
f"{value.versioned_id}:<property>"
|
|
225
|
+
f"Reverse connection must have a value type that points to a view or view property, got {value}"
|
|
203
226
|
)
|
|
204
227
|
return value
|
|
205
228
|
|
|
@@ -214,8 +237,11 @@ class DMSProperty(SheetEntity):
|
|
|
214
237
|
|
|
215
238
|
class DMSContainer(SheetEntity):
|
|
216
239
|
container: ContainerEntity = Field(alias="Container")
|
|
240
|
+
name: str | None = Field(alias="Name", default=None)
|
|
241
|
+
description: str | None = Field(alias="Description", default=None)
|
|
217
242
|
reference: URLEntity | ReferenceEntity | None = Field(alias="Reference", default=None, union_mode="left_to_right")
|
|
218
243
|
constraint: ContainerEntityList | None = Field(None, alias="Constraint")
|
|
244
|
+
class_: ClassEntity = Field(alias="Class (linage)")
|
|
219
245
|
|
|
220
246
|
def as_container(self) -> dm.ContainerApply:
|
|
221
247
|
container_id = self.container.as_id()
|
|
@@ -240,9 +266,10 @@ class DMSContainer(SheetEntity):
|
|
|
240
266
|
if isinstance(constraint_obj, dm.RequiresConstraint):
|
|
241
267
|
constraints.append(ContainerEntity.from_id(constraint_obj.require))
|
|
242
268
|
# UniquenessConstraint it handled in the properties
|
|
269
|
+
container_entity = ContainerEntity.from_id(container.as_id())
|
|
243
270
|
return cls(
|
|
244
|
-
class_=
|
|
245
|
-
container=
|
|
271
|
+
class_=container_entity.as_class(),
|
|
272
|
+
container=container_entity,
|
|
246
273
|
name=container.name or None,
|
|
247
274
|
description=container.description,
|
|
248
275
|
constraint=constraints or None,
|
|
@@ -251,10 +278,13 @@ class DMSContainer(SheetEntity):
|
|
|
251
278
|
|
|
252
279
|
class DMSView(SheetEntity):
|
|
253
280
|
view: ViewEntity = Field(alias="View")
|
|
281
|
+
name: str | None = Field(alias="Name", default=None)
|
|
282
|
+
description: str | None = Field(alias="Description", default=None)
|
|
254
283
|
implements: ViewEntityList | None = Field(None, alias="Implements")
|
|
255
284
|
reference: URLEntity | ReferenceEntity | None = Field(alias="Reference", default=None, union_mode="left_to_right")
|
|
256
|
-
filter_:
|
|
257
|
-
in_model: bool = Field(True, alias="
|
|
285
|
+
filter_: HasDataFilter | NodeTypeFilter | None = Field(None, alias="Filter")
|
|
286
|
+
in_model: bool = Field(True, alias="In Model")
|
|
287
|
+
class_: ClassEntity = Field(alias="Class (linage)")
|
|
258
288
|
|
|
259
289
|
def as_view(self) -> dm.ViewApply:
|
|
260
290
|
view_id = self.view.as_id()
|
|
@@ -269,20 +299,17 @@ class DMSView(SheetEntity):
|
|
|
269
299
|
)
|
|
270
300
|
|
|
271
301
|
@classmethod
|
|
272
|
-
def from_view(cls, view: dm.ViewApply,
|
|
302
|
+
def from_view(cls, view: dm.ViewApply, in_model: bool) -> "DMSView":
|
|
303
|
+
view_entity = ViewEntity.from_id(view.as_id())
|
|
304
|
+
class_entity = view_entity.as_class(skip_version=True)
|
|
305
|
+
|
|
273
306
|
return cls(
|
|
274
|
-
class_=
|
|
275
|
-
view=
|
|
307
|
+
class_=class_entity,
|
|
308
|
+
view=view_entity,
|
|
276
309
|
description=view.description,
|
|
277
310
|
name=view.name,
|
|
278
|
-
implements=[
|
|
279
|
-
|
|
280
|
-
space=parent.space, externalId=parent.external_id, version=parent.version or _DEFAULT_VERSION
|
|
281
|
-
)
|
|
282
|
-
for parent in view.implements
|
|
283
|
-
]
|
|
284
|
-
or None,
|
|
285
|
-
in_model=view.as_id() in data_model_view_ids,
|
|
311
|
+
implements=[ViewEntity.from_id(parent, _DEFAULT_VERSION) for parent in view.implements] or None,
|
|
312
|
+
in_model=in_model,
|
|
286
313
|
)
|
|
287
314
|
|
|
288
315
|
|
|
@@ -293,6 +320,41 @@ class DMSRules(BaseRules):
|
|
|
293
320
|
containers: SheetList[DMSContainer] | None = Field(None, alias="Containers")
|
|
294
321
|
reference: "DMSRules | None" = Field(None, alias="Reference")
|
|
295
322
|
|
|
323
|
+
@field_validator("reference")
|
|
324
|
+
def check_reference_of_reference(cls, value: "DMSRules | None", info: ValidationInfo) -> "DMSRules | None":
|
|
325
|
+
if value is None:
|
|
326
|
+
return None
|
|
327
|
+
if value.reference is not None:
|
|
328
|
+
raise ValueError("Reference rules cannot have a reference")
|
|
329
|
+
if value.metadata.data_model_type == DataModelType.solution and (metadata := info.data.get("metadata")):
|
|
330
|
+
warnings.warn(
|
|
331
|
+
issues.dms.SolutionOnTopOfSolutionModelWarning(
|
|
332
|
+
metadata.as_data_model_id(), value.metadata.as_data_model_id()
|
|
333
|
+
),
|
|
334
|
+
stacklevel=2,
|
|
335
|
+
)
|
|
336
|
+
return value
|
|
337
|
+
|
|
338
|
+
@field_validator("views")
|
|
339
|
+
def matching_version_and_space(cls, value: SheetList[DMSView], info: ValidationInfo) -> SheetList[DMSView]:
|
|
340
|
+
if not (metadata := info.data.get("metadata")):
|
|
341
|
+
return value
|
|
342
|
+
model_version = metadata.version
|
|
343
|
+
if different_version := [view.view.as_id() for view in value if view.view.version != model_version]:
|
|
344
|
+
warnings.warn(issues.dms.ViewModelVersionNotMatchingWarning(different_version, model_version), stacklevel=2)
|
|
345
|
+
if different_space := [view.view.as_id() for view in value if view.view.space != metadata.space]:
|
|
346
|
+
warnings.warn(issues.dms.ViewModelSpaceNotMatchingWarning(different_space, metadata.space), stacklevel=2)
|
|
347
|
+
return value
|
|
348
|
+
|
|
349
|
+
@field_validator("views")
|
|
350
|
+
def matching_version(cls, value: SheetList[DMSView], info: ValidationInfo) -> SheetList[DMSView]:
|
|
351
|
+
if not (metadata := info.data.get("metadata")):
|
|
352
|
+
return value
|
|
353
|
+
model_version = metadata.version
|
|
354
|
+
if different_version := [view.view.as_id() for view in value if view.view.version != model_version]:
|
|
355
|
+
warnings.warn(issues.dms.ViewModelVersionNotMatchingWarning(different_version, model_version), stacklevel=2)
|
|
356
|
+
return value
|
|
357
|
+
|
|
296
358
|
@model_validator(mode="after")
|
|
297
359
|
def consistent_container_properties(self) -> "DMSRules":
|
|
298
360
|
container_properties_by_id: dict[tuple[ContainerEntity, str], list[tuple[int, DMSProperty]]] = defaultdict(list)
|
|
@@ -439,7 +501,7 @@ class DMSRules(BaseRules):
|
|
|
439
501
|
# Everything is allowed
|
|
440
502
|
return self
|
|
441
503
|
# Is an extension of an existing model.
|
|
442
|
-
user_schema = self.as_schema()
|
|
504
|
+
user_schema = self.as_schema(include_ref=False)
|
|
443
505
|
ref_schema = self.reference.as_schema()
|
|
444
506
|
new_containers = {container.as_id(): container for container in user_schema.containers}
|
|
445
507
|
existing_containers = {container.as_id(): container for container in ref_schema.containers}
|
|
@@ -540,70 +602,20 @@ class DMSRules(BaseRules):
|
|
|
540
602
|
raise issues.MultiValueError(errors)
|
|
541
603
|
return self
|
|
542
604
|
|
|
543
|
-
@model_serializer(mode="
|
|
544
|
-
def dms_rules_serialization(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
dumped = prop.model_dump(**kwargs)
|
|
558
|
-
for field_name in field_names:
|
|
559
|
-
if value := dumped.get(field_name):
|
|
560
|
-
dumped[field_name] = value.removeprefix(default_space).removesuffix(default_version_wrapped)
|
|
561
|
-
# Value type can have a property as well
|
|
562
|
-
dumped[value_type_name] = dumped[value_type_name].replace(default_version, "")
|
|
563
|
-
properties.append(dumped)
|
|
564
|
-
|
|
565
|
-
views = []
|
|
566
|
-
field_names = ["Class", "View", "Implements"] if info.by_alias else ["class_", "view", "implements"]
|
|
567
|
-
implements_name = "Implements" if info.by_alias else "implements"
|
|
568
|
-
for view in self.views:
|
|
569
|
-
dumped = view.model_dump(**kwargs)
|
|
570
|
-
for field_name in field_names:
|
|
571
|
-
if value := dumped.get(field_name):
|
|
572
|
-
dumped[field_name] = value.removeprefix(default_space).removesuffix(default_version_wrapped)
|
|
573
|
-
if value := dumped.get(implements_name):
|
|
574
|
-
dumped[implements_name] = ",".join(
|
|
575
|
-
parent.strip().removeprefix(default_space).removesuffix(default_version_wrapped)
|
|
576
|
-
for parent in value.split(",")
|
|
577
|
-
)
|
|
578
|
-
views.append(dumped)
|
|
579
|
-
|
|
580
|
-
containers = []
|
|
581
|
-
field_names = ["Class", "Container"] if info.by_alias else ["class_", "container"]
|
|
582
|
-
constraint_name = "Constraint" if info.by_alias else "constraint"
|
|
583
|
-
for container in self.containers or []:
|
|
584
|
-
dumped = container.model_dump(**kwargs)
|
|
585
|
-
for field_name in field_names:
|
|
586
|
-
if value := dumped.get(field_name):
|
|
587
|
-
dumped[field_name] = value.removeprefix(default_space).removesuffix(default_version_wrapped)
|
|
588
|
-
if value := dumped.get(constraint_name):
|
|
589
|
-
dumped[constraint_name] = ",".join(
|
|
590
|
-
constraint.strip().removeprefix(default_space).removesuffix(default_version_wrapped)
|
|
591
|
-
for constraint in value.split(",")
|
|
592
|
-
)
|
|
593
|
-
containers.append(dumped)
|
|
594
|
-
|
|
595
|
-
output = {
|
|
596
|
-
"Metadata" if info.by_alias else "metadata": self.metadata.model_dump(**kwargs),
|
|
597
|
-
"Properties" if info.by_alias else "properties": properties,
|
|
598
|
-
"Views" if info.by_alias else "views": views,
|
|
599
|
-
"Containers" if info.by_alias else "containers": containers,
|
|
600
|
-
}
|
|
601
|
-
if self.reference is not None:
|
|
602
|
-
output["Reference" if info.by_alias else "reference"] = self.reference.model_dump(**kwargs)
|
|
603
|
-
return output
|
|
604
|
-
|
|
605
|
-
def as_schema(self, include_pipeline: bool = False, instance_space: str | None = None) -> DMSSchema:
|
|
606
|
-
return _DMSExporter(include_pipeline, instance_space).to_schema(self)
|
|
605
|
+
@model_serializer(mode="wrap", when_used="always")
|
|
606
|
+
def dms_rules_serialization(
|
|
607
|
+
self,
|
|
608
|
+
handler: Callable,
|
|
609
|
+
info: SerializationInfo,
|
|
610
|
+
) -> dict[str, Any]:
|
|
611
|
+
dumped = cast(dict[str, Any], handler(self, info))
|
|
612
|
+
space, version = self.metadata.space, self.metadata.version
|
|
613
|
+
return _DMSRulesSerializer(info, space, version).clean(dumped)
|
|
614
|
+
|
|
615
|
+
def as_schema(
|
|
616
|
+
self, include_ref: bool = False, include_pipeline: bool = False, instance_space: str | None = None
|
|
617
|
+
) -> DMSSchema:
|
|
618
|
+
return _DMSExporter(self, include_ref, include_pipeline, instance_space).to_schema()
|
|
607
619
|
|
|
608
620
|
def as_information_architect_rules(self) -> "InformationRules":
|
|
609
621
|
return _DMSRulesConverter(self).as_information_architect_rules()
|
|
@@ -640,21 +652,36 @@ class _DMSExporter:
|
|
|
640
652
|
instance_space (str): The space to use for the instance. Defaults to None,`Rules.metadata.space` will be used
|
|
641
653
|
"""
|
|
642
654
|
|
|
643
|
-
def __init__(
|
|
655
|
+
def __init__(
|
|
656
|
+
self,
|
|
657
|
+
rules: DMSRules,
|
|
658
|
+
include_ref: bool = True,
|
|
659
|
+
include_pipeline: bool = False,
|
|
660
|
+
instance_space: str | None = None,
|
|
661
|
+
):
|
|
662
|
+
self.include_ref = include_ref
|
|
644
663
|
self.include_pipeline = include_pipeline
|
|
645
664
|
self.instance_space = instance_space
|
|
665
|
+
self.rules = rules
|
|
666
|
+
self._ref_schema = rules.reference.as_schema() if rules.reference else None
|
|
667
|
+
if self._ref_schema:
|
|
668
|
+
# We skip version as that will always be missing in the reference
|
|
669
|
+
self._ref_views_by_id = {dm.ViewId(view.space, view.external_id): view for view in self._ref_schema.views}
|
|
670
|
+
else:
|
|
671
|
+
self._ref_views_by_id = {}
|
|
646
672
|
|
|
647
|
-
def to_schema(self
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
containers = self._create_containers(
|
|
673
|
+
def to_schema(self) -> DMSSchema:
|
|
674
|
+
rules = self.rules
|
|
675
|
+
container_properties_by_id, view_properties_by_id = self._gather_properties()
|
|
676
|
+
containers = self._create_containers(container_properties_by_id)
|
|
651
677
|
|
|
652
|
-
views, node_types = self._create_views_with_node_types(
|
|
678
|
+
views, node_types = self._create_views_with_node_types(view_properties_by_id)
|
|
653
679
|
|
|
654
680
|
views_not_in_model = {view.view.as_id() for view in rules.views if not view.in_model}
|
|
655
681
|
data_model = rules.metadata.as_data_model()
|
|
656
682
|
data_model.views = sorted(
|
|
657
|
-
[view_id for view_id in views.as_ids() if view_id not in views_not_in_model],
|
|
683
|
+
[view_id for view_id in views.as_ids() if view_id not in views_not_in_model],
|
|
684
|
+
key=lambda v: v.as_tuple(), # type: ignore[union-attr]
|
|
658
685
|
)
|
|
659
686
|
|
|
660
687
|
spaces = self._create_spaces(rules.metadata, containers, views, data_model)
|
|
@@ -668,6 +695,16 @@ class _DMSExporter:
|
|
|
668
695
|
)
|
|
669
696
|
if self.include_pipeline:
|
|
670
697
|
return PipelineSchema.from_dms(output, self.instance_space)
|
|
698
|
+
|
|
699
|
+
if self._ref_schema and self.include_ref:
|
|
700
|
+
output.frozen_ids.update(self._ref_schema.node_types.as_ids())
|
|
701
|
+
output.frozen_ids.update(self._ref_schema.views.as_ids())
|
|
702
|
+
output.frozen_ids.update(self._ref_schema.containers.as_ids())
|
|
703
|
+
output.node_types.extend(self._ref_schema.node_types)
|
|
704
|
+
output.views.extend(self._ref_schema.views)
|
|
705
|
+
output.containers.extend(self._ref_schema.containers)
|
|
706
|
+
output.data_models.extend(self._ref_schema.data_models)
|
|
707
|
+
|
|
671
708
|
return output
|
|
672
709
|
|
|
673
710
|
def _create_spaces(
|
|
@@ -691,11 +728,10 @@ class _DMSExporter:
|
|
|
691
728
|
|
|
692
729
|
def _create_views_with_node_types(
|
|
693
730
|
self,
|
|
694
|
-
dms_views: SheetList[DMSView],
|
|
695
731
|
view_properties_by_id: dict[dm.ViewId, list[DMSProperty]],
|
|
696
732
|
) -> tuple[dm.ViewApplyList, dm.NodeApplyList]:
|
|
697
|
-
views = dm.ViewApplyList([dms_view.as_view() for dms_view in
|
|
698
|
-
dms_view_by_id = {dms_view.view.as_id(): dms_view for dms_view in
|
|
733
|
+
views = dm.ViewApplyList([dms_view.as_view() for dms_view in self.rules.views])
|
|
734
|
+
dms_view_by_id = {dms_view.view.as_id(): dms_view for dms_view in self.rules.views}
|
|
699
735
|
|
|
700
736
|
for view in views:
|
|
701
737
|
view_id = view.as_id()
|
|
@@ -703,193 +739,66 @@ class _DMSExporter:
|
|
|
703
739
|
if not (view_properties := view_properties_by_id.get(view_id)):
|
|
704
740
|
continue
|
|
705
741
|
for prop in view_properties:
|
|
706
|
-
view_property
|
|
707
|
-
if
|
|
708
|
-
|
|
709
|
-
# a multi-edge connection.
|
|
710
|
-
if isinstance(prop.value_type, ViewEntity):
|
|
711
|
-
source_view_id = prop.value_type.as_id()
|
|
712
|
-
elif isinstance(prop.value_type, ViewPropertyEntity):
|
|
713
|
-
source_view_id = prop.value_type.as_view_id()
|
|
714
|
-
else:
|
|
715
|
-
raise ValueError(
|
|
716
|
-
"Direct relation must have a view as value type. "
|
|
717
|
-
"This should have been validated in the rules"
|
|
718
|
-
)
|
|
719
|
-
view_property = dm.MultiEdgeConnectionApply(
|
|
720
|
-
type=dm.DirectRelationReference(
|
|
721
|
-
space=source_view_id.space,
|
|
722
|
-
external_id=f"{prop.view.external_id}.{prop.view_property}",
|
|
723
|
-
),
|
|
724
|
-
source=source_view_id,
|
|
725
|
-
direction="outwards",
|
|
726
|
-
name=prop.name,
|
|
727
|
-
description=prop.description,
|
|
728
|
-
)
|
|
729
|
-
elif prop.container and prop.container_property and prop.view_property:
|
|
730
|
-
container_prop_identifier = prop.container_property
|
|
731
|
-
extra_args: dict[str, Any] = {}
|
|
732
|
-
if prop.relation == "direct" and isinstance(prop.value_type, ViewEntity):
|
|
733
|
-
extra_args["source"] = prop.value_type.as_id()
|
|
734
|
-
elif isinstance(prop.value_type, DMSUnknownEntity):
|
|
735
|
-
extra_args["source"] = None
|
|
736
|
-
elif prop.relation == "direct" and not isinstance(prop.value_type, ViewEntity):
|
|
737
|
-
raise ValueError(
|
|
738
|
-
"Direct relation must have a view as value type. "
|
|
739
|
-
"This should have been validated in the rules"
|
|
740
|
-
)
|
|
741
|
-
view_property = dm.MappedPropertyApply(
|
|
742
|
-
container=prop.container.as_id(),
|
|
743
|
-
container_property_identifier=container_prop_identifier,
|
|
744
|
-
name=prop.name,
|
|
745
|
-
description=prop.description,
|
|
746
|
-
**extra_args,
|
|
747
|
-
)
|
|
748
|
-
elif prop.view and prop.view_property and prop.relation == "multiedge":
|
|
749
|
-
if isinstance(prop.value_type, ViewEntity):
|
|
750
|
-
source_view_id = prop.value_type.as_id()
|
|
751
|
-
else:
|
|
752
|
-
raise ValueError(
|
|
753
|
-
"Multiedge relation must have a view as value type. "
|
|
754
|
-
"This should have been validated in the rules"
|
|
755
|
-
)
|
|
756
|
-
if isinstance(prop.reference, ReferenceEntity):
|
|
757
|
-
ref_view_prop = prop.reference.as_view_property_id()
|
|
758
|
-
edge_type = dm.DirectRelationReference(
|
|
759
|
-
space=ref_view_prop.source.space,
|
|
760
|
-
external_id=f"{ref_view_prop.source.external_id}.{ref_view_prop.property}",
|
|
761
|
-
)
|
|
762
|
-
else:
|
|
763
|
-
edge_type = dm.DirectRelationReference(
|
|
764
|
-
space=source_view_id.space,
|
|
765
|
-
external_id=f"{prop.view.external_id}.{prop.view_property}",
|
|
766
|
-
)
|
|
767
|
-
|
|
768
|
-
view_property = dm.MultiEdgeConnectionApply(
|
|
769
|
-
type=edge_type,
|
|
770
|
-
source=source_view_id,
|
|
771
|
-
direction="outwards",
|
|
772
|
-
name=prop.name,
|
|
773
|
-
description=prop.description,
|
|
774
|
-
)
|
|
775
|
-
elif prop.view and prop.view_property and prop.relation == "reversedirect":
|
|
776
|
-
if isinstance(prop.value_type, ViewPropertyEntity):
|
|
777
|
-
source_prop_id = prop.value_type.as_id()
|
|
778
|
-
source_view_id = cast(dm.ViewId, source_prop_id.source)
|
|
779
|
-
else:
|
|
780
|
-
raise ValueError(
|
|
781
|
-
"Reverse direct relation must have a view as value type. "
|
|
782
|
-
"This should have been validated in the rules"
|
|
783
|
-
)
|
|
784
|
-
source_prop = prop.value_type.property_
|
|
785
|
-
if source_prop is None:
|
|
786
|
-
raise ValueError(
|
|
787
|
-
"Reverse direct relation must set what it is the reverse property of. "
|
|
788
|
-
f"Which property in {prop.value_type.versioned_id} is this the reverse of? Expecting "
|
|
789
|
-
f"{prop.value_type.versioned_id}:<property>"
|
|
790
|
-
)
|
|
791
|
-
reverse_prop = next(
|
|
792
|
-
(
|
|
793
|
-
prop
|
|
794
|
-
for prop in view_properties_by_id.get(source_view_id, [])
|
|
795
|
-
if prop.property_ == source_prop
|
|
796
|
-
),
|
|
797
|
-
None,
|
|
798
|
-
)
|
|
799
|
-
if reverse_prop and reverse_prop.relation == "direct" and reverse_prop.is_list:
|
|
800
|
-
warnings.warn(
|
|
801
|
-
issues.dms.ReverseOfDirectRelationListWarning(view_id, prop.property_), stacklevel=2
|
|
802
|
-
)
|
|
803
|
-
if isinstance(reverse_prop.reference, ReferenceEntity):
|
|
804
|
-
ref_view_prop = reverse_prop.reference.as_view_property_id()
|
|
805
|
-
edge_type = dm.DirectRelationReference(
|
|
806
|
-
space=ref_view_prop.source.space,
|
|
807
|
-
external_id=f"{ref_view_prop.source.external_id}.{ref_view_prop.property}",
|
|
808
|
-
)
|
|
809
|
-
else:
|
|
810
|
-
edge_type = dm.DirectRelationReference(
|
|
811
|
-
space=source_prop_id.source.space,
|
|
812
|
-
external_id=f"{reverse_prop.view.external_id}.{reverse_prop.view_property}",
|
|
813
|
-
)
|
|
814
|
-
view_property = dm.MultiEdgeConnectionApply(
|
|
815
|
-
type=edge_type,
|
|
816
|
-
source=source_prop_id.source, # type: ignore[arg-type]
|
|
817
|
-
direction="inwards",
|
|
818
|
-
name=prop.name,
|
|
819
|
-
description=prop.description,
|
|
820
|
-
)
|
|
821
|
-
else:
|
|
822
|
-
args: dict[str, Any] = dict(
|
|
823
|
-
source=source_view_id,
|
|
824
|
-
through=dm.PropertyId(source_prop_id.source, source_prop),
|
|
825
|
-
description=prop.description,
|
|
826
|
-
name=prop.name,
|
|
827
|
-
)
|
|
828
|
-
reverse_direct_cls: dict[
|
|
829
|
-
bool | None,
|
|
830
|
-
type[dm.MultiReverseDirectRelationApply] | type[SingleReverseDirectRelationApply],
|
|
831
|
-
] = {
|
|
832
|
-
True: dm.MultiReverseDirectRelationApply,
|
|
833
|
-
False: SingleReverseDirectRelationApply,
|
|
834
|
-
None: dm.MultiReverseDirectRelationApply,
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
view_property = reverse_direct_cls[prop.is_list](**args)
|
|
838
|
-
elif prop.view and prop.view_property and prop.relation:
|
|
839
|
-
warnings.warn(
|
|
840
|
-
issues.dms.UnsupportedRelationWarning(view_id, prop.view_property, prop.relation), stacklevel=2
|
|
841
|
-
)
|
|
842
|
-
continue
|
|
843
|
-
else:
|
|
844
|
-
continue
|
|
845
|
-
prop_id = prop.view_property
|
|
846
|
-
view.properties[prop_id] = view_property
|
|
742
|
+
view_property = self._create_view_property(prop, view_properties_by_id)
|
|
743
|
+
if view_property is not None:
|
|
744
|
+
view.properties[prop.view_property] = view_property
|
|
847
745
|
|
|
848
|
-
|
|
746
|
+
data_model_type = self.rules.metadata.data_model_type
|
|
747
|
+
unique_node_types: set[dm.NodeId] = set()
|
|
849
748
|
parent_views = {parent for view in views for parent in view.implements or []}
|
|
850
749
|
for view in views:
|
|
851
|
-
ref_containers = sorted(view.referenced_containers(), key=lambda c: c.as_tuple())
|
|
852
750
|
dms_view = dms_view_by_id.get(view.as_id())
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
)
|
|
861
|
-
else:
|
|
862
|
-
node_type = dm.filters.Equals(["node", "type"], {"space": view.space, "externalId": view.external_id})
|
|
863
|
-
if view.as_id() in parent_views:
|
|
864
|
-
if dms_view and dms_view.filter_ == "nodeType":
|
|
751
|
+
dms_properties = view_properties_by_id.get(view.as_id(), [])
|
|
752
|
+
view_filter = self._create_view_filter(view, dms_view, data_model_type, dms_properties)
|
|
753
|
+
|
|
754
|
+
view.filter = view_filter.as_dms_filter()
|
|
755
|
+
|
|
756
|
+
if isinstance(view_filter, NodeTypeFilter):
|
|
757
|
+
unique_node_types.update(view_filter.nodes)
|
|
758
|
+
if view.as_id() in parent_views:
|
|
865
759
|
warnings.warn(issues.dms.NodeTypeFilterOnParentViewWarning(view.as_id()), stacklevel=2)
|
|
866
|
-
|
|
867
|
-
|
|
760
|
+
elif isinstance(view_filter, HasDataFilter) and data_model_type == DataModelType.solution:
|
|
761
|
+
if dms_view and isinstance(dms_view.reference, ReferenceEntity):
|
|
762
|
+
references = {dms_view.reference.as_view_id()}
|
|
763
|
+
elif any(True for prop in dms_properties if isinstance(prop.reference, ReferenceEntity)):
|
|
764
|
+
references = {
|
|
765
|
+
prop.reference.as_view_id()
|
|
766
|
+
for prop in dms_properties
|
|
767
|
+
if isinstance(prop.reference, ReferenceEntity)
|
|
768
|
+
}
|
|
868
769
|
else:
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
770
|
+
continue
|
|
771
|
+
warnings.warn(
|
|
772
|
+
issues.dms.HasDataFilterOnViewWithReferencesWarning(view.as_id(), list(references)), stacklevel=2
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
return views, dm.NodeApplyList(
|
|
776
|
+
[dm.NodeApply(space=node.space, external_id=node.external_id) for node in unique_node_types]
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
@classmethod
|
|
780
|
+
def _create_edge_type_from_prop(cls, prop: DMSProperty) -> dm.DirectRelationReference:
|
|
781
|
+
if isinstance(prop.reference, ReferenceEntity):
|
|
782
|
+
ref_view_prop = prop.reference.as_view_property_id()
|
|
783
|
+
return cls._create_edge_type_from_view_id(cast(dm.ViewId, ref_view_prop.source), ref_view_prop.property)
|
|
784
|
+
else:
|
|
785
|
+
return cls._create_edge_type_from_view_id(prop.view.as_id(), prop.view_property)
|
|
786
|
+
|
|
787
|
+
@staticmethod
|
|
788
|
+
def _create_edge_type_from_view_id(view_id: dm.ViewId, property_: str) -> dm.DirectRelationReference:
|
|
789
|
+
return dm.DirectRelationReference(
|
|
790
|
+
space=view_id.space,
|
|
791
|
+
# This is the same convention as used when converting GraphQL to DMS
|
|
792
|
+
external_id=f"{view_id.external_id}.{property_}",
|
|
793
|
+
)
|
|
886
794
|
|
|
887
795
|
def _create_containers(
|
|
888
796
|
self,
|
|
889
|
-
dms_container: SheetList[DMSContainer] | None,
|
|
890
797
|
container_properties_by_id: dict[dm.ContainerId, list[DMSProperty]],
|
|
891
798
|
) -> dm.ContainerApplyList:
|
|
892
|
-
containers = dm.ContainerApplyList(
|
|
799
|
+
containers = dm.ContainerApplyList(
|
|
800
|
+
[dms_container.as_container() for dms_container in self.rules.containers or []]
|
|
801
|
+
)
|
|
893
802
|
container_to_drop = set()
|
|
894
803
|
for container in containers:
|
|
895
804
|
container_id = container.as_id()
|
|
@@ -905,26 +814,14 @@ class _DMSExporter:
|
|
|
905
814
|
else:
|
|
906
815
|
type_cls = dm.DirectRelation
|
|
907
816
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
description=prop.description,
|
|
917
|
-
)
|
|
918
|
-
else:
|
|
919
|
-
type_: CognitePropertyType
|
|
920
|
-
type_ = type_cls(is_list=prop.is_list or False)
|
|
921
|
-
container.properties[prop_id] = dm.ContainerProperty(
|
|
922
|
-
type=type_,
|
|
923
|
-
nullable=prop.nullable if prop.nullable is not None else True,
|
|
924
|
-
default_value=prop.default,
|
|
925
|
-
name=prop.name,
|
|
926
|
-
description=prop.description,
|
|
927
|
-
)
|
|
817
|
+
type_ = type_cls(is_list=prop.is_list or False)
|
|
818
|
+
container.properties[prop.container_property] = dm.ContainerProperty(
|
|
819
|
+
type=type_,
|
|
820
|
+
nullable=prop.nullable if prop.nullable is not None else True,
|
|
821
|
+
default_value=prop.default,
|
|
822
|
+
name=prop.name,
|
|
823
|
+
description=prop.description,
|
|
824
|
+
)
|
|
928
825
|
|
|
929
826
|
uniqueness_properties: dict[str, set[str]] = defaultdict(set)
|
|
930
827
|
for prop in container_properties:
|
|
@@ -957,31 +854,175 @@ class _DMSExporter:
|
|
|
957
854
|
[container for container in containers if container.as_id() not in container_to_drop]
|
|
958
855
|
)
|
|
959
856
|
|
|
960
|
-
def _gather_properties(
|
|
961
|
-
self, rules: DMSRules
|
|
962
|
-
) -> tuple[dict[dm.ContainerId, list[DMSProperty]], dict[dm.ViewId, list[DMSProperty]]]:
|
|
857
|
+
def _gather_properties(self) -> tuple[dict[dm.ContainerId, list[DMSProperty]], dict[dm.ViewId, list[DMSProperty]]]:
|
|
963
858
|
container_properties_by_id: dict[dm.ContainerId, list[DMSProperty]] = defaultdict(list)
|
|
964
859
|
view_properties_by_id: dict[dm.ViewId, list[DMSProperty]] = defaultdict(list)
|
|
965
|
-
for prop in rules.properties:
|
|
860
|
+
for prop in self.rules.properties:
|
|
966
861
|
view_id = prop.view.as_id()
|
|
967
862
|
view_properties_by_id[view_id].append(prop)
|
|
968
863
|
|
|
969
864
|
if prop.container and prop.container_property:
|
|
970
|
-
if prop.relation == "direct" and prop.is_list:
|
|
971
|
-
warnings.warn(
|
|
972
|
-
issues.dms.DirectRelationListWarning(
|
|
973
|
-
container_id=prop.container.as_id(),
|
|
974
|
-
view_id=prop.view.as_id(),
|
|
975
|
-
property=prop.container_property,
|
|
976
|
-
),
|
|
977
|
-
stacklevel=2,
|
|
978
|
-
)
|
|
979
|
-
continue
|
|
980
865
|
container_id = prop.container.as_id()
|
|
981
866
|
container_properties_by_id[container_id].append(prop)
|
|
982
867
|
|
|
983
868
|
return container_properties_by_id, view_properties_by_id
|
|
984
869
|
|
|
870
|
+
def _create_view_filter(
|
|
871
|
+
self,
|
|
872
|
+
view: dm.ViewApply,
|
|
873
|
+
dms_view: DMSView | None,
|
|
874
|
+
data_model_type: DataModelType,
|
|
875
|
+
dms_properties: list[DMSProperty],
|
|
876
|
+
) -> DMSFilter:
|
|
877
|
+
selected_filter_name = (dms_view and dms_view.filter_ and dms_view.filter_.name) or ""
|
|
878
|
+
if dms_view and dms_view.filter_ and not dms_view.filter_.is_empty:
|
|
879
|
+
# Has Explicit Filter, use it
|
|
880
|
+
return dms_view.filter_
|
|
881
|
+
|
|
882
|
+
if data_model_type == DataModelType.solution and selected_filter_name in [NodeTypeFilter.name, ""]:
|
|
883
|
+
if (
|
|
884
|
+
dms_view
|
|
885
|
+
and isinstance(dms_view.reference, ReferenceEntity)
|
|
886
|
+
and not dms_properties
|
|
887
|
+
and (ref_view := self._ref_views_by_id.get(dms_view.reference.as_view_id()))
|
|
888
|
+
and ref_view.filter
|
|
889
|
+
):
|
|
890
|
+
# No new properties, only reference, reuse the reference filter
|
|
891
|
+
return DMSFilter.from_dms_filter(ref_view.filter)
|
|
892
|
+
else:
|
|
893
|
+
referenced_node_ids = {
|
|
894
|
+
prop.reference.as_node_entity()
|
|
895
|
+
for prop in dms_properties
|
|
896
|
+
if isinstance(prop.reference, ReferenceEntity)
|
|
897
|
+
}
|
|
898
|
+
if dms_view and isinstance(dms_view.reference, ReferenceEntity):
|
|
899
|
+
referenced_node_ids.add(dms_view.reference.as_node_entity())
|
|
900
|
+
if referenced_node_ids:
|
|
901
|
+
return NodeTypeFilter(inner=list(referenced_node_ids))
|
|
902
|
+
|
|
903
|
+
# Enterprise Model or (Solution + HasData)
|
|
904
|
+
ref_containers = view.referenced_containers()
|
|
905
|
+
if not ref_containers or selected_filter_name == HasDataFilter.name:
|
|
906
|
+
# Child filter without container properties
|
|
907
|
+
if selected_filter_name == HasDataFilter.name:
|
|
908
|
+
warnings.warn(issues.dms.HasDataFilterOnNoPropertiesViewWarning(view.as_id()), stacklevel=2)
|
|
909
|
+
return NodeTypeFilter(inner=[DMSNodeEntity(space=view.space, externalId=view.external_id)])
|
|
910
|
+
else:
|
|
911
|
+
# HasData or not provided (this is the default)
|
|
912
|
+
return HasDataFilter(inner=[ContainerEntity.from_id(id_) for id_ in ref_containers])
|
|
913
|
+
|
|
914
|
+
def _create_view_property(
|
|
915
|
+
self, prop: DMSProperty, view_properties_by_id: dict[dm.ViewId, list[DMSProperty]]
|
|
916
|
+
) -> ViewPropertyApply | None:
|
|
917
|
+
if prop.container and prop.container_property:
|
|
918
|
+
container_prop_identifier = prop.container_property
|
|
919
|
+
extra_args: dict[str, Any] = {}
|
|
920
|
+
if prop.connection == "direct":
|
|
921
|
+
if isinstance(prop.value_type, ViewEntity):
|
|
922
|
+
extra_args["source"] = prop.value_type.as_id()
|
|
923
|
+
elif isinstance(prop.value_type, DMSUnknownEntity):
|
|
924
|
+
extra_args["source"] = None
|
|
925
|
+
else:
|
|
926
|
+
# Should have been validated.
|
|
927
|
+
raise ValueError(
|
|
928
|
+
"If this error occurs it is a bug in NEAT, please report"
|
|
929
|
+
f"Debug Info, Invalid valueType direct: {prop.model_dump_json()}"
|
|
930
|
+
)
|
|
931
|
+
elif prop.connection is not None:
|
|
932
|
+
# Should have been validated.
|
|
933
|
+
raise ValueError(
|
|
934
|
+
"If this error occurs it is a bug in NEAT, please report"
|
|
935
|
+
f"Debug Info, Invalid connection: {prop.model_dump_json()}"
|
|
936
|
+
)
|
|
937
|
+
return dm.MappedPropertyApply(
|
|
938
|
+
container=prop.container.as_id(),
|
|
939
|
+
container_property_identifier=container_prop_identifier,
|
|
940
|
+
name=prop.name,
|
|
941
|
+
description=prop.description,
|
|
942
|
+
**extra_args,
|
|
943
|
+
)
|
|
944
|
+
elif prop.connection == "edge":
|
|
945
|
+
if isinstance(prop.value_type, ViewEntity):
|
|
946
|
+
source_view_id = prop.value_type.as_id()
|
|
947
|
+
else:
|
|
948
|
+
# Should have been validated.
|
|
949
|
+
raise ValueError(
|
|
950
|
+
"If this error occurs it is a bug in NEAT, please report"
|
|
951
|
+
f"Debug Info, Invalid valueType edge: {prop.model_dump_json()}"
|
|
952
|
+
)
|
|
953
|
+
edge_cls: type[dm.EdgeConnectionApply] = dm.MultiEdgeConnectionApply
|
|
954
|
+
# If is_list is not set, we default to a MultiEdgeConnection
|
|
955
|
+
if prop.is_list is False:
|
|
956
|
+
edge_cls = SingleEdgeConnectionApply
|
|
957
|
+
|
|
958
|
+
return edge_cls(
|
|
959
|
+
type=self._create_edge_type_from_prop(prop),
|
|
960
|
+
source=source_view_id,
|
|
961
|
+
direction="outwards",
|
|
962
|
+
name=prop.name,
|
|
963
|
+
description=prop.description,
|
|
964
|
+
)
|
|
965
|
+
elif prop.connection == "reverse":
|
|
966
|
+
reverse_prop_id: str | None = None
|
|
967
|
+
if isinstance(prop.value_type, ViewPropertyEntity):
|
|
968
|
+
source_view_id = prop.value_type.as_view_id()
|
|
969
|
+
reverse_prop_id = prop.value_type.property_
|
|
970
|
+
elif isinstance(prop.value_type, ViewEntity):
|
|
971
|
+
source_view_id = prop.value_type.as_id()
|
|
972
|
+
else:
|
|
973
|
+
# Should have been validated.
|
|
974
|
+
raise ValueError(
|
|
975
|
+
"If this error occurs it is a bug in NEAT, please report"
|
|
976
|
+
f"Debug Info, Invalid valueType reverse connection: {prop.model_dump_json()}"
|
|
977
|
+
)
|
|
978
|
+
reverse_prop: DMSProperty | None = None
|
|
979
|
+
if reverse_prop_id is not None:
|
|
980
|
+
reverse_prop = next(
|
|
981
|
+
(
|
|
982
|
+
prop
|
|
983
|
+
for prop in view_properties_by_id.get(source_view_id, [])
|
|
984
|
+
if prop.property_ == reverse_prop_id
|
|
985
|
+
),
|
|
986
|
+
None,
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
if reverse_prop is None:
|
|
990
|
+
warnings.warn(
|
|
991
|
+
issues.dms.ReverseRelationMissingOtherSideWarning(source_view_id, prop.view_property),
|
|
992
|
+
stacklevel=2,
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
if reverse_prop is None or reverse_prop.connection == "edge":
|
|
996
|
+
inwards_edge_cls = (
|
|
997
|
+
dm.MultiEdgeConnectionApply if prop.is_list in [True, None] else SingleEdgeConnectionApply
|
|
998
|
+
)
|
|
999
|
+
return inwards_edge_cls(
|
|
1000
|
+
type=self._create_edge_type_from_prop(reverse_prop or prop),
|
|
1001
|
+
source=source_view_id,
|
|
1002
|
+
name=prop.name,
|
|
1003
|
+
description=prop.description,
|
|
1004
|
+
direction="inwards",
|
|
1005
|
+
)
|
|
1006
|
+
elif reverse_prop_id and reverse_prop and reverse_prop.connection == "direct":
|
|
1007
|
+
reverse_direct_cls = (
|
|
1008
|
+
dm.MultiReverseDirectRelationApply if prop.is_list is True else SingleReverseDirectRelationApply
|
|
1009
|
+
)
|
|
1010
|
+
return reverse_direct_cls(
|
|
1011
|
+
source=source_view_id,
|
|
1012
|
+
through=dm.PropertyId(source=source_view_id, property=reverse_prop_id),
|
|
1013
|
+
name=prop.name,
|
|
1014
|
+
description=prop.description,
|
|
1015
|
+
)
|
|
1016
|
+
else:
|
|
1017
|
+
return None
|
|
1018
|
+
|
|
1019
|
+
elif prop.view and prop.view_property and prop.connection:
|
|
1020
|
+
warnings.warn(
|
|
1021
|
+
issues.dms.UnsupportedConnectionWarning(prop.view.as_id(), prop.view_property, prop.connection or ""),
|
|
1022
|
+
stacklevel=2,
|
|
1023
|
+
)
|
|
1024
|
+
return None
|
|
1025
|
+
|
|
985
1026
|
|
|
986
1027
|
class _DMSRulesConverter:
|
|
987
1028
|
def __init__(self, dms: DMSRules):
|
|
@@ -1092,3 +1133,123 @@ class _DMSRulesConverter:
|
|
|
1092
1133
|
suffix=container.suffix,
|
|
1093
1134
|
property=property_.container_property,
|
|
1094
1135
|
)
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
class _DMSRulesSerializer:
|
|
1139
|
+
# These are the fields that need to be cleaned from the default space and version
|
|
1140
|
+
PROPERTIES_FIELDS: ClassVar[list[str]] = ["class_", "view", "value_type", "container"]
|
|
1141
|
+
VIEWS_FIELDS: ClassVar[list[str]] = ["class_", "view", "implements"]
|
|
1142
|
+
CONTAINERS_FIELDS: ClassVar[list[str]] = ["class_", "container"]
|
|
1143
|
+
|
|
1144
|
+
def __init__(self, info: SerializationInfo, default_space: str, default_version: str) -> None:
|
|
1145
|
+
self.default_space = f"{default_space}:"
|
|
1146
|
+
self.default_version = f"version={default_version}"
|
|
1147
|
+
self.default_version_wrapped = f"({self.default_version})"
|
|
1148
|
+
|
|
1149
|
+
self.properties_fields = self.PROPERTIES_FIELDS
|
|
1150
|
+
self.views_fields = self.VIEWS_FIELDS
|
|
1151
|
+
self.containers_fields = self.CONTAINERS_FIELDS
|
|
1152
|
+
self.prop_name = "properties"
|
|
1153
|
+
self.view_name = "views"
|
|
1154
|
+
self.container_name = "containers"
|
|
1155
|
+
self.metadata_name = "metadata"
|
|
1156
|
+
self.prop_view = "view"
|
|
1157
|
+
self.prop_view_property = "view_property"
|
|
1158
|
+
self.prop_value_type = "value_type"
|
|
1159
|
+
self.view_view = "view"
|
|
1160
|
+
self.view_implements = "implements"
|
|
1161
|
+
self.container_container = "container"
|
|
1162
|
+
self.container_constraint = "constraint"
|
|
1163
|
+
|
|
1164
|
+
if info.by_alias:
|
|
1165
|
+
self.properties_fields = [
|
|
1166
|
+
DMSProperty.model_fields[field].alias or field for field in self.properties_fields
|
|
1167
|
+
]
|
|
1168
|
+
self.views_fields = [DMSView.model_fields[field].alias or field for field in self.views_fields]
|
|
1169
|
+
self.container_fields = [
|
|
1170
|
+
DMSContainer.model_fields[field].alias or field for field in self.containers_fields
|
|
1171
|
+
]
|
|
1172
|
+
self.prop_view = DMSProperty.model_fields[self.prop_view].alias or self.prop_view
|
|
1173
|
+
self.prop_view_property = DMSProperty.model_fields[self.prop_view_property].alias or self.prop_view_property
|
|
1174
|
+
self.prop_value_type = DMSProperty.model_fields[self.prop_value_type].alias or self.prop_value_type
|
|
1175
|
+
self.view_view = DMSView.model_fields[self.view_view].alias or self.view_view
|
|
1176
|
+
self.view_implements = DMSView.model_fields[self.view_implements].alias or self.view_implements
|
|
1177
|
+
self.container_container = (
|
|
1178
|
+
DMSContainer.model_fields[self.container_container].alias or self.container_container
|
|
1179
|
+
)
|
|
1180
|
+
self.container_constraint = (
|
|
1181
|
+
DMSContainer.model_fields[self.container_constraint].alias or self.container_constraint
|
|
1182
|
+
)
|
|
1183
|
+
self.prop_name = DMSRules.model_fields[self.prop_name].alias or self.prop_name
|
|
1184
|
+
self.view_name = DMSRules.model_fields[self.view_name].alias or self.view_name
|
|
1185
|
+
self.container_name = DMSRules.model_fields[self.container_name].alias or self.container_name
|
|
1186
|
+
self.metadata_name = DMSRules.model_fields[self.metadata_name].alias or self.metadata_name
|
|
1187
|
+
|
|
1188
|
+
if isinstance(info.exclude, dict):
|
|
1189
|
+
# Just for happy mypy
|
|
1190
|
+
exclude = cast(dict, info.exclude)
|
|
1191
|
+
self.exclude_properties = exclude.get("properties", {}).get("__all__", set())
|
|
1192
|
+
self.exclude_views = exclude.get("views", {}).get("__all__", set()) or set()
|
|
1193
|
+
self.exclude_containers = exclude.get("containers", {}).get("__all__", set()) or set()
|
|
1194
|
+
self.metadata_exclude = exclude.get("metadata", set()) or set()
|
|
1195
|
+
self.exclude_top = {k for k, v in exclude.items() if not v}
|
|
1196
|
+
else:
|
|
1197
|
+
self.exclude_top = set(info.exclude or {})
|
|
1198
|
+
self.exclude_properties = set()
|
|
1199
|
+
self.exclude_views = set()
|
|
1200
|
+
self.exclude_containers = set()
|
|
1201
|
+
self.metadata_exclude = set()
|
|
1202
|
+
|
|
1203
|
+
def clean(self, dumped: dict[str, Any]) -> dict[str, Any]:
|
|
1204
|
+
# Sorting to get a deterministic order
|
|
1205
|
+
dumped[self.prop_name] = sorted(
|
|
1206
|
+
dumped[self.prop_name]["data"], key=lambda p: (p[self.prop_view], p[self.prop_view_property])
|
|
1207
|
+
)
|
|
1208
|
+
dumped[self.view_name] = sorted(dumped[self.view_name]["data"], key=lambda v: v[self.view_view])
|
|
1209
|
+
if self.container_name in dumped:
|
|
1210
|
+
dumped[self.container_name] = sorted(
|
|
1211
|
+
dumped[self.container_name]["data"], key=lambda c: c[self.container_container]
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
for prop in dumped[self.prop_name]:
|
|
1215
|
+
for field_name in self.properties_fields:
|
|
1216
|
+
if value := prop.get(field_name):
|
|
1217
|
+
prop[field_name] = value.removeprefix(self.default_space).removesuffix(self.default_version_wrapped)
|
|
1218
|
+
# Value type can have a property as well
|
|
1219
|
+
prop[self.prop_value_type] = prop[self.prop_value_type].replace(self.default_version, "")
|
|
1220
|
+
if self.exclude_properties:
|
|
1221
|
+
for field in self.exclude_properties:
|
|
1222
|
+
prop.pop(field, None)
|
|
1223
|
+
|
|
1224
|
+
for view in dumped[self.view_name]:
|
|
1225
|
+
for field_name in self.views_fields:
|
|
1226
|
+
if value := view.get(field_name):
|
|
1227
|
+
view[field_name] = value.removeprefix(self.default_space).removesuffix(self.default_version_wrapped)
|
|
1228
|
+
if value := view.get(self.view_implements):
|
|
1229
|
+
view[self.view_implements] = ",".join(
|
|
1230
|
+
parent.strip().removeprefix(self.default_space).removesuffix(self.default_version_wrapped)
|
|
1231
|
+
for parent in value.split(",")
|
|
1232
|
+
)
|
|
1233
|
+
if self.exclude_views:
|
|
1234
|
+
for field in self.exclude_views:
|
|
1235
|
+
view.pop(field, None)
|
|
1236
|
+
|
|
1237
|
+
for container in dumped[self.container_name]:
|
|
1238
|
+
for field_name in self.containers_fields:
|
|
1239
|
+
if value := container.get(field_name):
|
|
1240
|
+
container[field_name] = value.removeprefix(self.default_space)
|
|
1241
|
+
|
|
1242
|
+
if value := container.get(self.container_constraint):
|
|
1243
|
+
container[self.container_constraint] = ",".join(
|
|
1244
|
+
constraint.strip().removeprefix(self.default_space) for constraint in value.split(",")
|
|
1245
|
+
)
|
|
1246
|
+
if self.exclude_containers:
|
|
1247
|
+
for field in self.exclude_containers:
|
|
1248
|
+
container.pop(field, None)
|
|
1249
|
+
|
|
1250
|
+
if self.metadata_exclude:
|
|
1251
|
+
for field in self.metadata_exclude:
|
|
1252
|
+
dumped[self.metadata_name].pop(field, None)
|
|
1253
|
+
for field in self.exclude_top:
|
|
1254
|
+
dumped.pop(field, None)
|
|
1255
|
+
return dumped
|