cognite-neat 0.76.1__py3-none-any.whl → 0.76.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/app/api/routers/core.py +1 -1
  3. cognite/neat/app/api/routers/rules.py +1 -1
  4. cognite/neat/graph/extractors/_mock_graph_generator.py +2 -2
  5. cognite/neat/rules/_shared.py +1 -1
  6. cognite/neat/rules/analysis/_information_rules.py +3 -3
  7. cognite/neat/rules/exporters/_base.py +1 -1
  8. cognite/neat/rules/exporters/_rules2dms.py +8 -49
  9. cognite/neat/rules/exporters/_rules2excel.py +9 -3
  10. cognite/neat/rules/exporters/_rules2ontology.py +2 -2
  11. cognite/neat/rules/exporters/_rules2yaml.py +1 -1
  12. cognite/neat/rules/exporters/_validation.py +2 -2
  13. cognite/neat/rules/importers/_base.py +1 -1
  14. cognite/neat/rules/importers/_dms2rules.py +93 -108
  15. cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +1 -1
  16. cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +2 -3
  17. cognite/neat/rules/importers/_owl2rules/_owl2classes.py +1 -1
  18. cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +2 -2
  19. cognite/neat/rules/importers/_owl2rules/_owl2properties.py +1 -1
  20. cognite/neat/rules/importers/_owl2rules/_owl2rules.py +1 -1
  21. cognite/neat/rules/importers/_spreadsheet2rules.py +10 -4
  22. cognite/neat/rules/importers/_yaml2rules.py +3 -3
  23. cognite/neat/rules/issues/base.py +5 -0
  24. cognite/neat/rules/models/__init__.py +27 -0
  25. cognite/neat/rules/models/dms/__init__.py +18 -0
  26. cognite/neat/rules/models/dms/_converter.py +140 -0
  27. cognite/neat/rules/models/dms/_exporter.py +405 -0
  28. cognite/neat/rules/models/dms/_rules.py +379 -0
  29. cognite/neat/rules/models/{rules/_dms_rules_write.py → dms/_rules_input.py} +33 -33
  30. cognite/neat/rules/models/{rules/_dms_schema.py → dms/_schema.py} +10 -4
  31. cognite/neat/rules/models/dms/_serializer.py +126 -0
  32. cognite/neat/rules/models/dms/_validation.py +255 -0
  33. cognite/neat/rules/models/information/__init__.py +3 -0
  34. cognite/neat/rules/models/information/_converter.py +193 -0
  35. cognite/neat/rules/models/{rules/_information_rules.py → information/_rules.py} +35 -202
  36. cognite/neat/workflows/steps/data_contracts.py +1 -1
  37. cognite/neat/workflows/steps/lib/current/rules_exporter.py +9 -3
  38. cognite/neat/workflows/steps/lib/current/rules_importer.py +1 -1
  39. cognite/neat/workflows/steps/lib/current/rules_validator.py +1 -2
  40. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/METADATA +1 -1
  41. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/RECORD +50 -43
  42. cognite/neat/rules/models/rules/__init__.py +0 -14
  43. cognite/neat/rules/models/rules/_dms_architect_rules.py +0 -1255
  44. /cognite/neat/rules/models/{rules/_base.py → _base.py} +0 -0
  45. /cognite/neat/rules/models/{rdfpath.py → _rdfpath.py} +0 -0
  46. /cognite/neat/rules/models/{rules/_types → _types}/__init__.py +0 -0
  47. /cognite/neat/rules/models/{rules/_types → _types}/_base.py +0 -0
  48. /cognite/neat/rules/models/{rules/_types → _types}/_field.py +0 -0
  49. /cognite/neat/rules/models/{rules/_domain_rules.py → domain.py} +0 -0
  50. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/LICENSE +0 -0
  51. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/WHEEL +0 -0
  52. {cognite_neat-0.76.1.dist-info → cognite_neat-0.76.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,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 ._base import DataModelType, ExtensionCategory, SchemaCompleteness
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 DMSMetadataWrite:
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) -> "DMSMetadataWrite | 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 DMSPropertyWrite:
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]) -> "DMSPropertyWrite": ...
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["DMSPropertyWrite"]: ...
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
- ) -> "DMSPropertyWrite | list[DMSPropertyWrite] | None":
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 DMSContainerWrite:
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]) -> "DMSContainerWrite": ...
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["DMSContainerWrite"]: ...
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
- ) -> "DMSContainerWrite | list[DMSContainerWrite] | None":
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 DMSViewWrite:
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]) -> "DMSViewWrite": ...
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["DMSViewWrite"]: ...
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) -> "DMSViewWrite | list[DMSViewWrite] | 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,46 +298,46 @@ class DMSViewWrite:
298
298
 
299
299
 
300
300
  @dataclass
301
- class DMSRulesWrite:
302
- metadata: DMSMetadataWrite
303
- properties: Sequence[DMSPropertyWrite]
304
- views: Sequence[DMSViewWrite]
305
- containers: Sequence[DMSContainerWrite] | None = None
306
- reference: "DMSRulesWrite | DMSRules | None" = None
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
307
307
 
308
308
  @classmethod
309
309
  @overload
310
- def load(cls, data: dict[str, Any]) -> "DMSRulesWrite": ...
310
+ def load(cls, data: dict[str, Any]) -> "DMSRulesInput": ...
311
311
 
312
312
  @classmethod
313
313
  @overload
314
314
  def load(cls, data: None) -> None: ...
315
315
 
316
316
  @classmethod
317
- def load(cls, data: dict | None) -> "DMSRulesWrite | None":
317
+ def load(cls, data: dict | None) -> "DMSRulesInput | None":
318
318
  if data is None:
319
319
  return None
320
320
  _add_alias(data, DMSRules)
321
321
  return cls(
322
- metadata=DMSMetadataWrite.load(data.get("metadata")), # type: ignore[arg-type]
323
- properties=DMSPropertyWrite.load(data.get("properties")), # type: ignore[arg-type]
324
- views=DMSViewWrite.load(data.get("views")), # type: ignore[arg-type]
325
- containers=DMSContainerWrite.load(data.get("containers")) or [],
326
- reference=DMSRulesWrite.load(data.get("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")),
327
327
  )
328
328
 
329
- def as_read(self) -> DMSRules:
329
+ def as_rules(self) -> DMSRules:
330
330
  return DMSRules.model_validate(self.dump())
331
331
 
332
332
  def dump(self) -> dict[str, Any]:
333
333
  default_space = self.metadata.space
334
334
  default_version = self.metadata.version
335
335
  reference: dict[str, Any] | None = None
336
- if isinstance(self.reference, DMSRulesWrite):
336
+ if isinstance(self.reference, DMSRulesInput):
337
337
  reference = self.reference.dump()
338
338
  elif isinstance(self.reference, DMSRules):
339
- # We need to load through the DMSRulesWrite to set the correct default space and version
340
- reference = DMSRulesWrite.load(self.reference.model_dump()).dump()
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()
341
341
 
342
342
  return dict(
343
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 frozen ids are parts of the schema that should not be modified or deleted.
50
- # This is used the exporting the schema.
51
- frozen_ids: set[dm.ViewId | dm.ContainerId | dm.NodeId] = field(default_factory=set)
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: