cognite-neat 0.108.0__py3-none-any.whl → 0.109.1__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.

Files changed (44) hide show
  1. cognite/neat/_constants.py +1 -1
  2. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +8 -4
  3. cognite/neat/_graph/queries/_base.py +4 -0
  4. cognite/neat/_graph/transformers/__init__.py +3 -3
  5. cognite/neat/_graph/transformers/_base.py +4 -4
  6. cognite/neat/_graph/transformers/_classic_cdf.py +13 -13
  7. cognite/neat/_graph/transformers/_prune_graph.py +3 -3
  8. cognite/neat/_graph/transformers/_rdfpath.py +3 -4
  9. cognite/neat/_graph/transformers/_value_type.py +23 -16
  10. cognite/neat/_issues/errors/__init__.py +2 -0
  11. cognite/neat/_issues/errors/_external.py +8 -0
  12. cognite/neat/_issues/warnings/_resources.py +1 -1
  13. cognite/neat/_rules/exporters/_rules2yaml.py +1 -1
  14. cognite/neat/_rules/importers/_rdf/_inference2rules.py +179 -118
  15. cognite/neat/_rules/models/_base_rules.py +9 -8
  16. cognite/neat/_rules/models/dms/_exporter.py +5 -4
  17. cognite/neat/_rules/transformers/__init__.py +4 -3
  18. cognite/neat/_rules/transformers/_base.py +6 -1
  19. cognite/neat/_rules/transformers/_converters.py +436 -361
  20. cognite/neat/_rules/transformers/_mapping.py +4 -4
  21. cognite/neat/_session/_base.py +71 -69
  22. cognite/neat/_session/_create.py +133 -0
  23. cognite/neat/_session/_drop.py +55 -1
  24. cognite/neat/_session/_fix.py +28 -0
  25. cognite/neat/_session/_inspect.py +20 -6
  26. cognite/neat/_session/_mapping.py +8 -8
  27. cognite/neat/_session/_prepare.py +3 -247
  28. cognite/neat/_session/_read.py +78 -4
  29. cognite/neat/_session/_set.py +34 -12
  30. cognite/neat/_session/_show.py +14 -41
  31. cognite/neat/_session/_state.py +48 -51
  32. cognite/neat/_session/_to.py +7 -3
  33. cognite/neat/_session/exceptions.py +7 -1
  34. cognite/neat/_store/_graph_store.py +14 -13
  35. cognite/neat/_store/_provenance.py +36 -20
  36. cognite/neat/_store/_rules_store.py +172 -293
  37. cognite/neat/_store/exceptions.py +40 -4
  38. cognite/neat/_utils/auth.py +4 -2
  39. cognite/neat/_version.py +1 -1
  40. {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.1.dist-info}/METADATA +1 -1
  41. {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.1.dist-info}/RECORD +44 -42
  42. {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.1.dist-info}/LICENSE +0 -0
  43. {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.1.dist-info}/WHEEL +0 -0
  44. {cognite_neat-0.108.0.dist-info → cognite_neat-0.109.1.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,3 @@
1
- import dataclasses
2
1
  import re
3
2
  import warnings
4
3
  from abc import ABC
@@ -9,12 +8,14 @@ from typing import ClassVar, Literal, TypeVar, cast, overload
9
8
 
10
9
  from cognite.client.data_classes import data_modeling as dms
11
10
  from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier, ViewId
11
+ from cognite.client.utils.useful_types import SequenceNotStr
12
12
  from rdflib import Namespace
13
13
 
14
14
  from cognite.neat._client import NeatClient
15
15
  from cognite.neat._client.data_classes.data_modeling import ContainerApplyDict, ViewApplyDict
16
16
  from cognite.neat._constants import (
17
17
  COGNITE_MODELS,
18
+ COGNITE_SPACES,
18
19
  DMS_CONTAINER_PROPERTY_SIZE_LIMIT,
19
20
  DMS_RESERVED_PROPERTIES,
20
21
  get_default_prefixes_and_namespaces,
@@ -27,8 +28,6 @@ from cognite.neat._issues.warnings._models import (
27
28
  )
28
29
  from cognite.neat._rules._shared import (
29
30
  ReadInputRules,
30
- ReadRules,
31
- T_InputRules,
32
31
  VerifiedRules,
33
32
  )
34
33
  from cognite.neat._rules.analysis import DMSAnalysis
@@ -50,38 +49,29 @@ from cognite.neat._rules.models.entities import (
50
49
  ContainerEntity,
51
50
  DMSUnknownEntity,
52
51
  EdgeEntity,
53
- Entity,
54
52
  HasDataFilter,
55
53
  MultiValueTypeInfo,
56
54
  ReverseConnectionEntity,
57
- T_Entity,
58
55
  UnknownEntity,
59
56
  ViewEntity,
60
57
  )
61
58
  from cognite.neat._rules.models.information import InformationClass, InformationMetadata, InformationProperty
62
- from cognite.neat._rules.models.information._rules_input import (
63
- InformationInputClass,
64
- InformationInputProperty,
65
- InformationInputRules,
66
- )
67
59
  from cognite.neat._utils.text import to_camel
68
60
 
69
- from ._base import RulesTransformer
61
+ from ._base import T_VerifiedIn, T_VerifiedOut, VerifiedRulesTransformer
70
62
  from ._verification import VerifyDMSRules
71
63
 
72
- T_VerifiedInRules = TypeVar("T_VerifiedInRules", bound=VerifiedRules)
73
- T_VerifiedOutRules = TypeVar("T_VerifiedOutRules", bound=VerifiedRules)
74
64
  T_InputInRules = TypeVar("T_InputInRules", bound=ReadInputRules)
75
65
  T_InputOutRules = TypeVar("T_InputOutRules", bound=ReadInputRules)
76
66
 
77
67
 
78
- class ConversionTransformer(RulesTransformer[T_VerifiedInRules, T_VerifiedOutRules], ABC):
68
+ class ConversionTransformer(VerifiedRulesTransformer[T_VerifiedIn, T_VerifiedOut], ABC):
79
69
  """Base class for all conversion transformers."""
80
70
 
81
71
  ...
82
72
 
83
73
 
84
- class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], ReadRules[InformationInputRules]]): # type: ignore[misc]
74
+ class ToCompliantEntities(VerifiedRulesTransformer[InformationRules, InformationRules]): # type: ignore[misc]
85
75
  """Converts input rules to rules with compliant entity IDs that match regex patters used
86
76
  by DMS schema components."""
87
77
 
@@ -89,13 +79,11 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
89
79
  def description(self) -> str:
90
80
  return "Ensures externalIDs are compliant with CDF"
91
81
 
92
- def transform(self, rules: ReadRules[InformationInputRules]) -> ReadRules[InformationInputRules]:
93
- if rules.rules is None:
94
- return rules
95
- copy: InformationInputRules = dataclasses.replace(rules.rules)
82
+ def transform(self, rules: InformationRules) -> InformationRules:
83
+ copy = rules.model_copy(deep=True)
96
84
  copy.classes = self._fix_classes(copy.classes)
97
85
  copy.properties = self._fix_properties(copy.properties)
98
- return ReadRules(copy, rules.read_context)
86
+ return copy
99
87
 
100
88
  @classmethod
101
89
  def _fix_entity(cls, entity: str) -> str:
@@ -112,16 +100,8 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
112
100
  return re.sub(r"[^a-zA-Z0-9]+", "_", entity)
113
101
 
114
102
  @classmethod
115
- def _fix_class(cls, class_: str | ClassEntity) -> str | ClassEntity:
116
- if isinstance(class_, str):
117
- if len(class_.split(":")) == 2:
118
- prefix, suffix = class_.split(":")
119
- class_ = f"{cls._fix_entity(prefix)}:{cls._fix_entity(suffix)}"
120
-
121
- else:
122
- class_ = cls._fix_entity(class_)
123
-
124
- elif isinstance(class_, ClassEntity) and type(class_.prefix) is str:
103
+ def _fix_class(cls, class_: ClassEntity) -> ClassEntity:
104
+ if isinstance(class_, ClassEntity) and type(class_.prefix) is str:
125
105
  class_ = ClassEntity(
126
106
  prefix=cls._fix_entity(class_.prefix),
127
107
  suffix=cls._fix_entity(class_.suffix),
@@ -131,28 +111,17 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
131
111
 
132
112
  @classmethod
133
113
  def _fix_value_type(
134
- cls, value_type: str | DataType | ClassEntity | MultiValueTypeInfo
135
- ) -> str | DataType | ClassEntity | MultiValueTypeInfo:
136
- fixed_value_type: str | DataType | ClassEntity | MultiValueTypeInfo
137
-
138
- if isinstance(value_type, str):
139
- # this is a multi value type but as string
140
- if " | " in value_type:
141
- value_types = value_type.split(" | ")
142
- fixed_value_type = " | ".join([cast(str, cls._fix_value_type(v)) for v in value_types])
143
- # this is value type specified with prefix:suffix string
144
- elif ":" in value_type:
145
- fixed_value_type = cls._fix_class(value_type)
146
-
147
- # this is value type specified as suffix only
148
- else:
149
- fixed_value_type = cls._fix_entity(value_type)
114
+ cls, value_type: DataType | ClassEntity | MultiValueTypeInfo
115
+ ) -> DataType | ClassEntity | MultiValueTypeInfo:
116
+ fixed_value_type: DataType | ClassEntity | MultiValueTypeInfo
150
117
 
151
- # value type specified as instances of DataType, ClassEntity or MultiValueTypeInfo
152
- elif isinstance(value_type, MultiValueTypeInfo):
118
+ # value type specified as MultiValueTypeInfo
119
+ if isinstance(value_type, MultiValueTypeInfo):
153
120
  fixed_value_type = MultiValueTypeInfo(
154
121
  types=[cast(DataType | ClassEntity, cls._fix_value_type(type_)) for type_ in value_type.types],
155
122
  )
123
+
124
+ # value type specified as ClassEntity instance
156
125
  elif isinstance(value_type, ClassEntity):
157
126
  fixed_value_type = cls._fix_class(value_type)
158
127
 
@@ -163,16 +132,16 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
163
132
  return fixed_value_type
164
133
 
165
134
  @classmethod
166
- def _fix_classes(cls, definitions: list[InformationInputClass]) -> list[InformationInputClass]:
167
- fixed_definitions = []
135
+ def _fix_classes(cls, definitions: SheetList[InformationClass]) -> SheetList[InformationClass]:
136
+ fixed_definitions = SheetList[InformationClass]()
168
137
  for definition in definitions:
169
138
  definition.class_ = cls._fix_class(definition.class_)
170
139
  fixed_definitions.append(definition)
171
140
  return fixed_definitions
172
141
 
173
142
  @classmethod
174
- def _fix_properties(cls, definitions: list[InformationInputProperty]) -> list[InformationInputProperty]:
175
- fixed_definitions = []
143
+ def _fix_properties(cls, definitions: SheetList[InformationProperty]) -> SheetList[InformationProperty]:
144
+ fixed_definitions = SheetList[InformationProperty]()
176
145
  for definition in definitions:
177
146
  definition.class_ = cls._fix_class(definition.class_)
178
147
  definition.property_ = cls._fix_entity(definition.property_)
@@ -181,80 +150,107 @@ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], Rea
181
150
  return fixed_definitions
182
151
 
183
152
 
184
- class PrefixEntities(RulesTransformer[ReadRules[T_InputRules], ReadRules[T_InputRules]]): # type: ignore[type-var]
185
- """Prefixes all entities with a given prefix."""
153
+ class PrefixEntities(ConversionTransformer): # type: ignore[type-var]
154
+ """Prefixes all entities with a given prefix if they are in the same space as data model."""
186
155
 
187
156
  def __init__(self, prefix: str) -> None:
188
157
  self._prefix = prefix
189
158
 
190
159
  @property
191
160
  def description(self) -> str:
192
- return f"Prefixes all views with {self._prefix!r}"
161
+ return f"Prefixes all entities with {self._prefix!r} prefix if they are in the same space as data model."
193
162
 
194
- def transform(self, rules: ReadRules[T_InputRules]) -> ReadRules[T_InputRules]:
195
- in_ = rules.rules
196
- if in_ is None:
197
- return rules
198
- copy: T_InputRules = dataclasses.replace(in_)
199
- if isinstance(copy, InformationInputRules):
200
- prefixed_by_class: dict[str, str] = {}
163
+ @overload
164
+ def transform(self, rules: DMSRules) -> DMSRules: ...
165
+
166
+ @overload
167
+ def transform(self, rules: InformationRules) -> InformationRules: ...
168
+
169
+ def transform(self, rules: InformationRules | DMSRules) -> InformationRules | DMSRules:
170
+ copy: InformationRules | DMSRules = rules.model_copy(deep=True)
171
+
172
+ # Case: Prefix Information Rules
173
+ if isinstance(copy, InformationRules):
174
+ # prefix classes
201
175
  for cls in copy.classes:
202
- prefixed = str(self._with_prefix(cls.class_))
203
- prefixed_by_class[str(cls.class_)] = prefixed
204
- cls.class_ = prefixed
176
+ if cls.class_.prefix == copy.metadata.prefix:
177
+ cls.class_ = self._with_prefix(cls.class_)
178
+
179
+ if cls.implements:
180
+ # prefix parents
181
+ for i, parent_class in enumerate(cls.implements):
182
+ if parent_class.prefix == copy.metadata.prefix:
183
+ cls.implements[i] = self._with_prefix(parent_class)
184
+
205
185
  for prop in copy.properties:
206
- prop.class_ = self._with_prefix(prop.class_)
207
- if str(prop.value_type) in prefixed_by_class:
208
- prop.value_type = prefixed_by_class[str(prop.value_type)]
209
- return ReadRules(copy, rules.read_context) # type: ignore[arg-type]
210
- elif isinstance(copy, DMSInputRules):
211
- prefixed_by_view: dict[str, str] = {}
186
+ if prop.class_.prefix == copy.metadata.prefix:
187
+ prop.class_ = self._with_prefix(prop.class_)
188
+
189
+ # value type property is not multi and it is ClassEntity
190
+
191
+ if isinstance(prop.value_type, ClassEntity) and prop.value_type.prefix == copy.metadata.prefix:
192
+ prop.value_type = self._with_prefix(cast(ClassEntity, prop.value_type))
193
+ elif isinstance(prop.value_type, MultiValueTypeInfo):
194
+ for i, value_type in enumerate(prop.value_type.types):
195
+ if isinstance(value_type, ClassEntity) and value_type.prefix == copy.metadata.prefix:
196
+ prop.value_type.types[i] = self._with_prefix(cast(ClassEntity, value_type))
197
+ return copy
198
+
199
+ # Case: Prefix DMS Rules
200
+ elif isinstance(copy, DMSRules):
212
201
  for view in copy.views:
213
- prefixed = str(self._with_prefix(view.view))
214
- prefixed_by_view[str(view.view)] = prefixed
215
- view.view = prefixed
202
+ if view.view.space == copy.metadata.space:
203
+ view.view = self._with_prefix(view.view)
204
+
205
+ if view.implements:
206
+ for i, parent_view in enumerate(view.implements):
207
+ if parent_view.space == copy.metadata.space:
208
+ view.implements[i] = self._with_prefix(parent_view)
209
+
216
210
  for dms_prop in copy.properties:
217
- dms_prop.view = self._with_prefix(dms_prop.view)
218
- if str(dms_prop.value_type) in prefixed_by_view:
219
- dms_prop.value_type = prefixed_by_view[str(dms_prop.value_type)]
211
+ if dms_prop.view.space == copy.metadata.space:
212
+ dms_prop.view = self._with_prefix(dms_prop.view)
213
+
214
+ if isinstance(dms_prop.value_type, ViewEntity) and dms_prop.value_type.space == copy.metadata.space:
215
+ dms_prop.value_type = self._with_prefix(dms_prop.value_type)
216
+
217
+ if isinstance(dms_prop.container, ContainerEntity) and dms_prop.container.space == copy.metadata.space:
218
+ dms_prop.container = self._with_prefix(dms_prop.container)
219
+
220
220
  if copy.containers:
221
221
  for container in copy.containers:
222
- container.container = self._with_prefix(container.container)
223
- return ReadRules(copy, rules.read_context)
222
+ if container.container.space == copy.metadata.space:
223
+ container.container = self._with_prefix(container.container)
224
+ return copy
225
+
224
226
  raise NeatValueError(f"Unsupported rules type: {type(copy)}")
225
227
 
226
228
  @overload
227
- def _with_prefix(self, raw: str) -> str: ...
229
+ def _with_prefix(self, entity: ClassEntity) -> ClassEntity: ...
228
230
 
229
231
  @overload
230
- def _with_prefix(self, raw: T_Entity) -> T_Entity: ...
231
-
232
- def _with_prefix(self, raw: str | T_Entity) -> str | T_Entity:
233
- is_entity_format = not isinstance(raw, str)
234
- entity = Entity.load(raw)
235
- output: ClassEntity | ViewEntity | ContainerEntity
236
- if isinstance(entity, ClassEntity):
237
- output = ClassEntity(prefix=entity.prefix, suffix=f"{self._prefix}{entity.suffix}", version=entity.version)
238
- elif isinstance(entity, ViewEntity):
239
- output = ViewEntity(
240
- space=entity.space, externalId=f"{self._prefix}{entity.external_id}", version=entity.version
241
- )
242
- elif isinstance(entity, ContainerEntity):
243
- output = ContainerEntity(space=entity.space, externalId=f"{self._prefix}{entity.external_id}")
244
- elif isinstance(entity, UnknownEntity | Entity):
245
- return f"{self._prefix}{raw}"
232
+ def _with_prefix(self, entity: ViewEntity) -> ViewEntity: ...
233
+
234
+ @overload
235
+ def _with_prefix(self, entity: ContainerEntity) -> ContainerEntity: ...
236
+
237
+ def _with_prefix(
238
+ self, entity: ViewEntity | ContainerEntity | ClassEntity
239
+ ) -> ViewEntity | ContainerEntity | ClassEntity:
240
+ if isinstance(entity, ViewEntity | ContainerEntity | ClassEntity):
241
+ entity.suffix = f"{self._prefix}{entity.suffix}"
242
+
246
243
  else:
247
244
  raise NeatValueError(f"Unsupported entity type: {type(entity)}")
248
- if is_entity_format:
249
- return cast(T_Entity, output)
250
- return str(output)
245
+
246
+ return entity
251
247
 
252
248
 
253
249
  class InformationToDMS(ConversionTransformer[InformationRules, DMSRules]):
254
250
  """Converts InformationRules to DMSRules."""
255
251
 
256
252
  def __init__(
257
- self, ignore_undefined_value_types: bool = False, reserved_properties: Literal["error", "skip"] = "error"
253
+ self, ignore_undefined_value_types: bool = False, reserved_properties: Literal["error", "warning"] = "error"
258
254
  ):
259
255
  self.ignore_undefined_value_types = ignore_undefined_value_types
260
256
  self.reserved_properties = reserved_properties
@@ -294,7 +290,7 @@ class ConvertToRules(ConversionTransformer[VerifiedRules, VerifiedRules]):
294
290
  _T_Entity = TypeVar("_T_Entity", bound=ClassEntity | ViewEntity)
295
291
 
296
292
 
297
- class SetIDDMSModel(RulesTransformer[DMSRules, DMSRules]):
293
+ class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
298
294
  def __init__(self, new_id: DataModelId | tuple[str, str, str]):
299
295
  self.new_id = DataModelId.load(new_id)
300
296
 
@@ -314,77 +310,17 @@ class SetIDDMSModel(RulesTransformer[DMSRules, DMSRules]):
314
310
  return DMSRules.model_validate(DMSInputRules.load(dump).dump())
315
311
 
316
312
 
317
- class ToExtensionModel(RulesTransformer[DMSRules, DMSRules], ABC):
313
+ class ToExtensionModel(VerifiedRulesTransformer[DMSRules, DMSRules], ABC):
318
314
  type_: ClassVar[str]
319
315
 
320
- def __init__(self, new_model_id: DataModelIdentifier, org_name: str, dummy_property: str | None = None) -> None:
316
+ def __init__(self, new_model_id: DataModelIdentifier) -> None:
321
317
  self.new_model_id = DataModelId.load(new_model_id)
322
318
  if not self.new_model_id.version:
323
319
  raise NeatValueError("Version is required for the new model.")
324
- self.org_name = org_name
325
- self.dummy_property = dummy_property
326
320
 
327
321
  @property
328
322
  def description(self) -> str:
329
- return f"Prepared data model {self.new_model_id} to be {self.type_.replace('_', ' ')} data model."
330
-
331
- def _create_new_views(
332
- self, rules: DMSRules
333
- ) -> tuple[SheetList[DMSView], SheetList[DMSContainer], SheetList[DMSProperty]]:
334
- """Creates new views for the new model.
335
-
336
- If the dummy property is provided, it will also create a new container for each view
337
- with a single property that is the dummy property.
338
- """
339
- new_views = SheetList[DMSView]()
340
- new_containers = SheetList[DMSContainer]()
341
- new_properties = SheetList[DMSProperty]()
342
-
343
- for definition in rules.views:
344
- view_entity = self._remove_cognite_affix(definition.view)
345
- view_entity.version = cast(str, self.new_model_id.version)
346
- view_entity.prefix = self.new_model_id.space
347
-
348
- new_views.append(
349
- DMSView(
350
- view=view_entity,
351
- implements=[definition.view],
352
- in_model=True,
353
- name=definition.name,
354
- )
355
- )
356
- if self.dummy_property is None:
357
- continue
358
-
359
- container_entity = ContainerEntity(space=view_entity.prefix, externalId=view_entity.external_id)
360
-
361
- container = DMSContainer(container=container_entity)
362
-
363
- prefix = to_camel(view_entity.suffix)
364
- property_ = DMSProperty(
365
- view=view_entity,
366
- view_property=f"{prefix}{self.dummy_property}",
367
- value_type=String(),
368
- nullable=True,
369
- immutable=False,
370
- is_list=False,
371
- container=container_entity,
372
- container_property=f"{prefix}{self.dummy_property}",
373
- )
374
-
375
- new_properties.append(property_)
376
- new_containers.append(container)
377
-
378
- return new_views, new_containers, new_properties
379
-
380
- def _remove_cognite_affix(self, entity: _T_Entity) -> _T_Entity:
381
- """This method removes `Cognite` affix from the entity."""
382
- new_suffix = entity.suffix.replace("Cognite", self.org_name or "")
383
- if isinstance(entity, ViewEntity):
384
- return ViewEntity(space=entity.space, externalId=new_suffix, version=entity.version) # type: ignore[return-value]
385
- elif isinstance(entity, ClassEntity):
386
- return ClassEntity(prefix=entity.prefix, suffix=new_suffix, version=entity.version) # type: ignore[return-value]
387
- raise ValueError(f"Unsupported entity type: {type(entity)}")
323
+ return f"Create new data model {self.new_model_id} of type {self.type_.replace('_', ' ')} data model."
388
324
 
389
325
 
390
326
  class ToEnterpriseModel(ToExtensionModel):
@@ -397,7 +333,9 @@ class ToEnterpriseModel(ToExtensionModel):
397
333
  dummy_property: str = "GUID",
398
334
  move_connections: bool = False,
399
335
  ):
400
- super().__init__(new_model_id, org_name, dummy_property)
336
+ super().__init__(new_model_id)
337
+ self.dummy_property = dummy_property
338
+ self.org_name = org_name
401
339
  self.move_connections = move_connections
402
340
 
403
341
  def transform(self, rules: DMSRules) -> DMSRules:
@@ -411,13 +349,8 @@ class ToEnterpriseModel(ToExtensionModel):
411
349
  return self._to_enterprise(rules)
412
350
 
413
351
  def _to_enterprise(self, reference_model: DMSRules) -> DMSRules:
414
- dump = reference_model.dump()
352
+ enterprise_model = reference_model.model_copy(deep=True)
415
353
 
416
- # This will create reference model components in the enterprise model space
417
- enterprise_model = DMSRules.model_validate(DMSInputRules.load(dump).dump())
418
-
419
- # Post validation metadata update:
420
- enterprise_model.metadata.name = self.type_
421
354
  enterprise_model.metadata.name = f"{self.org_name} {self.type_} data model"
422
355
  enterprise_model.metadata.space = self.new_model_id.space
423
356
  enterprise_model.metadata.external_id = self.new_model_id.external_id
@@ -432,43 +365,91 @@ class ToEnterpriseModel(ToExtensionModel):
432
365
 
433
366
  if self.move_connections:
434
367
  # Move connections from reference model to new enterprise model
435
- enterprise_properties.extend(self._move_connections(enterprise_model))
368
+ enterprise_properties.extend(self._create_connection_properties(enterprise_model, enterprise_views))
436
369
 
437
370
  # ... however, we do not want to keep the reference containers and properties
371
+ # these we are getting for free through the implements.
438
372
  enterprise_model.containers = enterprise_containers
439
373
  enterprise_model.properties = enterprise_properties
440
374
 
441
375
  return enterprise_model
442
376
 
443
377
  @staticmethod
444
- def _move_connections(rules: DMSRules) -> SheetList[DMSProperty]:
445
- implements: dict[ViewEntity, list[ViewEntity]] = defaultdict(list)
378
+ def _create_connection_properties(rules: DMSRules, new_views: SheetList[DMSView]) -> SheetList[DMSProperty]:
379
+ """Creates a new connection property for each connection property in the reference model.
380
+
381
+ This is for example when you create an enterprise model from CogniteCore, you ensure that your
382
+ new Asset, Equipment, TimeSeries, Activity, and File views all point to each other.
383
+ """
384
+ # Note all new news have an implements attribute that points to the original view
385
+ previous_by_new_view = {view.implements[0]: view.view for view in new_views if view.implements}
386
+ connection_properties = SheetList[DMSProperty]()
387
+ for prop in rules.properties:
388
+ if (
389
+ isinstance(prop.value_type, ViewEntity)
390
+ and prop.view in previous_by_new_view
391
+ and prop.value_type in previous_by_new_view
392
+ ):
393
+ new_property = prop.model_copy(deep=True)
394
+ new_property.view = previous_by_new_view[prop.view]
395
+ new_property.value_type = previous_by_new_view[prop.value_type]
396
+ connection_properties.append(new_property)
397
+
398
+ return connection_properties
399
+
400
+ def _create_new_views(
401
+ self, rules: DMSRules
402
+ ) -> tuple[SheetList[DMSView], SheetList[DMSContainer], SheetList[DMSProperty]]:
403
+ """Creates new views for the new model.
404
+
405
+ If the dummy property is provided, it will also create a new container for each view
406
+ with a single property that is the dummy property.
407
+ """
408
+ new_views = SheetList[DMSView]()
409
+ new_containers = SheetList[DMSContainer]()
446
410
  new_properties = SheetList[DMSProperty]()
447
411
 
448
- for view in rules.views:
449
- if view.view.space == rules.metadata.space and view.implements:
450
- for implemented_view in view.implements:
451
- implements.setdefault(implemented_view, []).append(view.view)
452
-
453
- # currently only supporting single implementation of reference view in enterprise view
454
- # connections that do not have properties
455
- if all(len(v) == 1 for v in implements.values()):
456
- for prop_ in rules.properties:
457
- if (
458
- prop_.view.space != rules.metadata.space
459
- and prop_.connection
460
- and isinstance(prop_.value_type, ViewEntity)
461
- and implements.get(prop_.view)
462
- and implements.get(prop_.value_type)
463
- ):
464
- if isinstance(prop_.connection, EdgeEntity) and prop_.connection.properties:
465
- continue
466
- new_property = prop_.model_copy(deep=True)
467
- new_property.view = implements[prop_.view][0]
468
- new_property.value_type = implements[prop_.value_type][0]
469
- new_properties.append(new_property)
470
-
471
- return new_properties
412
+ for definition in rules.views:
413
+ view_entity = self._remove_cognite_affix(definition.view)
414
+ view_entity.version = cast(str, self.new_model_id.version)
415
+ view_entity.prefix = self.new_model_id.space
416
+ new_views.append(
417
+ DMSView(
418
+ view=view_entity,
419
+ implements=[definition.view],
420
+ in_model=True,
421
+ name=definition.name,
422
+ )
423
+ )
424
+
425
+ if self.dummy_property is None:
426
+ continue
427
+
428
+ container_entity = ContainerEntity(space=view_entity.prefix, externalId=view_entity.external_id)
429
+
430
+ container = DMSContainer(container=container_entity)
431
+
432
+ property_id = f"{to_camel(view_entity.suffix)}{self.dummy_property}"
433
+ property_ = DMSProperty(
434
+ view=view_entity,
435
+ view_property=property_id,
436
+ value_type=String(),
437
+ nullable=True,
438
+ immutable=False,
439
+ is_list=False,
440
+ container=container_entity,
441
+ container_property=property_id,
442
+ )
443
+
444
+ new_properties.append(property_)
445
+ new_containers.append(container)
446
+
447
+ return new_views, new_containers, new_properties
448
+
449
+ def _remove_cognite_affix(self, entity: ViewEntity) -> ViewEntity:
450
+ """This method removes `Cognite` affix from the entity."""
451
+ new_suffix = entity.suffix.replace("Cognite", self.org_name)
452
+ return ViewEntity(space=entity.space, externalId=new_suffix, version=entity.version)
472
453
 
473
454
 
474
455
  class ToSolutionModel(ToExtensionModel):
@@ -478,18 +459,10 @@ class ToSolutionModel(ToExtensionModel):
478
459
 
479
460
  Args:
480
461
  new_model_id: DataData model identifier for the new model.
481
- org_name: If the existing model is a Cognite Data Model, this will replace the "Cognite" affix.
482
- mode: The mode of the solution model. Either "read" or "write". A "write" model will create a new
483
- container for each view with a dummy property. Read mode will only inherit the view filter from the
484
- original model.
485
462
  dummy_property: Only applicable if mode='write'. The identifier of the dummy property in the newly created
486
463
  container.
487
464
  exclude_views_in_other_spaces: Whether to exclude views that are not in the same space as the existing model,
488
465
  when creating the solution model.
489
- filter_type: If mode="read", this is the type of filter to apply to the new views. The filter is used to
490
- ensure that the new views will return the same instance as the original views. The view filter is the
491
- simplest filter, but it has limitation in the fusion UI. The container filter is in essence a more
492
- verbose version of the view filter, and it has better support in the fusion UI. The default is "container".
493
466
 
494
467
  """
495
468
 
@@ -498,152 +471,71 @@ class ToSolutionModel(ToExtensionModel):
498
471
  def __init__(
499
472
  self,
500
473
  new_model_id: DataModelIdentifier,
501
- org_name: str = "My",
502
- mode: Literal["read", "write"] = "read",
474
+ properties: Literal["repeat", "connection"] = "connection",
503
475
  dummy_property: str | None = "GUID",
504
- exclude_views_in_other_spaces: bool = True,
476
+ direct_property: str = "enterprise",
477
+ view_prefix: str = "Enterprise",
505
478
  filter_type: Literal["container", "view"] = "container",
479
+ exclude_views_in_other_spaces: bool = False,
480
+ skip_cognite_views: bool = True,
506
481
  ):
507
- super().__init__(new_model_id, org_name, dummy_property if mode == "write" else None)
508
- self.mode = mode
509
- self.exclude_views_in_other_spaces = exclude_views_in_other_spaces
482
+ super().__init__(new_model_id)
483
+ self.properties = properties
484
+ self.dummy_property = dummy_property
485
+ self.direct_property = direct_property
486
+ self.view_prefix = view_prefix
510
487
  self.filter_type = filter_type
488
+ self.exclude_views_in_other_spaces = exclude_views_in_other_spaces
489
+ self.skip_cognite_views = skip_cognite_views
511
490
 
512
491
  def transform(self, rules: DMSRules) -> DMSRules:
513
492
  reference_model = rules
514
493
  reference_model_id = reference_model.metadata.as_data_model_id()
515
-
516
- # if model is solution then we need to get correct space for views and containers
517
- if self.mode not in ["read", "write"]:
518
- raise NeatValueError(f"Unsupported mode: {self.mode}")
519
-
520
494
  if reference_model_id in COGNITE_MODELS:
521
495
  warnings.warn(
522
496
  SolutionModelBuildOnTopOfCDMWarning(reference_model_id=reference_model_id),
523
497
  stacklevel=2,
524
498
  )
525
-
526
499
  return self._to_solution(reference_model)
527
500
 
528
- @staticmethod
529
- def _has_views_in_multiple_space(rules: DMSRules) -> bool:
530
- return any(view.view.space != rules.metadata.space for view in rules.views)
531
-
532
501
  def _to_solution(self, reference_rules: DMSRules) -> DMSRules:
533
502
  """For creation of solution data model / rules specifically for mapping over existing containers."""
503
+ reference_rules = self._expand_properties(reference_rules.model_copy(deep=True))
534
504
 
535
- dump = reference_rules.dump(entities_exclude_defaults=True)
536
-
537
- # Prepare new model metadata prior validation
538
- # Since we dropped the defaults, all entities will update the space and version
539
- # to the new model space and version
540
- dump["metadata"]["name"] = f"{self.org_name} {self.type_} data model"
541
- dump["metadata"]["space"] = self.new_model_id.space
542
- dump["metadata"]["external_id"] = self.new_model_id.external_id
543
- dump["metadata"]["version"] = self.new_model_id.version
544
-
545
- solution_model = DMSRules.model_validate(DMSInputRules.load(dump).dump())
546
-
547
- # This is not desirable for the containers, so we manually fix that here.
548
- # It is easier to change the space for all entities and then revert the containers, than
549
- # to change the space for all entities except the containers.
550
- for prop in solution_model.properties:
551
- if prop.container and prop.container.space == self.new_model_id.space:
552
- # If the container is in the new model space, we want to map it to the reference model space
553
- # This is reverting the .dump() -> .load() above.
554
- prop.container = ContainerEntity(
555
- space=reference_rules.metadata.space,
556
- externalId=prop.container.suffix,
505
+ new_views, new_properties, read_view_by_new_view = self._create_views(reference_rules)
506
+ new_containers, new_container_properties = self._create_containers_update_view_filter(
507
+ new_views, reference_rules, read_view_by_new_view
508
+ )
509
+ new_properties.extend(new_container_properties)
510
+
511
+ if self.properties == "connection":
512
+ # Ensure the Enterprise view and the new solution view are next to each other
513
+ new_properties.sort(
514
+ key=lambda prop: (
515
+ prop.view.external_id.removeprefix(self.view_prefix),
516
+ bool(prop.view.external_id.startswith(self.view_prefix)),
557
517
  )
558
-
559
- for view in solution_model.views:
560
- view.implements = None
561
-
562
- if self.exclude_views_in_other_spaces and self._has_views_in_multiple_space(reference_rules):
563
- solution_model.views = SheetList[DMSView](
564
- [view for view in solution_model.views if view.view.space == solution_model.metadata.space]
565
518
  )
566
-
567
- # Dropping containers coming from reference model
568
- solution_model.containers = None
569
-
570
- # If reference model on which we are mapping one of Cognite Data Models
571
- # since we want to affix these with the organization name
572
- if reference_rules.metadata.as_data_model_id() in COGNITE_MODELS:
573
- # Remove Cognite affix in view external_id / suffix.
574
- for prop in solution_model.properties:
575
- prop.view = self._remove_cognite_affix(prop.view)
576
- if isinstance(prop.value_type, ViewEntity):
577
- prop.value_type = self._remove_cognite_affix(prop.value_type)
578
- for view in solution_model.views:
579
- view.view = self._remove_cognite_affix(view.view)
580
- if view.implements:
581
- view.implements = [self._remove_cognite_affix(implemented) for implemented in view.implements]
582
-
583
- if self.mode == "write":
584
- _, new_containers, new_properties = self._create_new_views(solution_model)
585
- # Here we add ONLY dummy properties of the solution model and
586
- # corresponding solution model space containers to hold them
587
- solution_model.containers = new_containers
588
- solution_model.properties.extend(new_properties)
589
- elif self.mode == "read":
590
- # Inherit view filter from original model to ensure the same instances are returned
591
- # when querying the new view.
592
- ref_views_by_external_id = {
593
- view.view.external_id: view
594
- for view in reference_rules.views
595
- if view.view.space == reference_rules.metadata.space
596
- }
597
- ref_containers_by_ref_view = defaultdict(set)
598
- for prop in reference_rules.properties:
599
- if prop.container:
600
- ref_containers_by_ref_view[prop.view].add(prop.container)
601
- for view in solution_model.views:
602
- if ref_view := ref_views_by_external_id.get(view.view.external_id):
603
- if self.filter_type == "view":
604
- view.filter_ = HasDataFilter(inner=[ref_view.view])
605
- elif self.filter_type == "container" and (
606
- ref_containers := ref_containers_by_ref_view.get(ref_view.view)
607
- ):
608
- # Sorting to ensure deterministic order
609
- view.filter_ = HasDataFilter(inner=sorted(ref_containers))
610
-
611
- return solution_model
612
-
613
-
614
- class ToDataProductModel(ToSolutionModel):
615
- type_: ClassVar[str] = "data_product"
616
-
617
- def __init__(
618
- self,
619
- new_model_id: DataModelIdentifier,
620
- org_name: str = "My",
621
- include: Literal["same-space", "all"] = "same-space",
622
- ):
623
- super().__init__(new_model_id, org_name, mode="read", dummy_property=None, exclude_views_in_other_spaces=False)
624
- self.include = include
625
-
626
- def transform(self, rules: DMSRules) -> DMSRules:
627
- # Copy to ensure immutability
628
- expanded = self._expand_properties(rules.model_copy(deep=True))
629
- if self.include == "same-space":
630
- expanded.views = SheetList[DMSView](
631
- [view for view in expanded.views if view.view.space == expanded.metadata.space]
632
- )
633
- used_view_entities = {view.view for view in expanded.views}
634
- expanded.properties = SheetList[DMSProperty](
635
- [
636
- prop
637
- for prop in expanded.properties
638
- if prop.view.space == expanded.metadata.space
639
- and (
640
- (isinstance(prop.value_type, ViewEntity) and prop.value_type in used_view_entities)
641
- or not isinstance(prop.value_type, ViewEntity)
642
- )
643
- ]
644
- )
645
-
646
- return self._to_solution(expanded)
519
+ else:
520
+ new_properties.sort(key=lambda prop: (prop.view.external_id, prop.view_property))
521
+
522
+ metadata = reference_rules.metadata.model_copy(
523
+ deep=True,
524
+ update={
525
+ "space": self.new_model_id.space,
526
+ "external_id": self.new_model_id.external_id,
527
+ "version": self.new_model_id.version,
528
+ "name": f"{self.type_} data model",
529
+ },
530
+ )
531
+ return DMSRules(
532
+ metadata=metadata,
533
+ properties=new_properties,
534
+ views=new_views,
535
+ containers=new_containers or None,
536
+ enum=reference_rules.enum,
537
+ nodes=reference_rules.nodes,
538
+ )
647
539
 
648
540
  @staticmethod
649
541
  def _expand_properties(rules: DMSRules) -> DMSRules:
@@ -667,8 +559,169 @@ class ToDataProductModel(ToSolutionModel):
667
559
  property_ids.add(prop.view_property)
668
560
  return rules
669
561
 
562
+ def _create_views(
563
+ self, reference: DMSRules
564
+ ) -> tuple[SheetList[DMSView], SheetList[DMSProperty], dict[ViewEntity, ViewEntity]]:
565
+ renaming: dict[ViewEntity, ViewEntity] = {}
566
+ new_views = SheetList[DMSView]()
567
+ read_view_by_new_view: dict[ViewEntity, ViewEntity] = {}
568
+ skipped_views: set[ViewEntity] = set()
569
+ for ref_view in reference.views:
570
+ if (self.skip_cognite_views and ref_view.view.space in COGNITE_SPACES) or (
571
+ self.exclude_views_in_other_spaces and ref_view.view.space != reference.metadata.space
572
+ ):
573
+ skipped_views.add(ref_view.view)
574
+ continue
575
+ new_entity = ViewEntity(
576
+ # MyPy we validate that version is string in the constructor
577
+ space=self.new_model_id.space,
578
+ externalId=ref_view.view.external_id,
579
+ version=self.new_model_id.version, # type: ignore[arg-type]
580
+ )
581
+ if self.properties == "connection":
582
+ # Set suffix to existing view and introduce a new view.
583
+ # This will be used to point to the one view in the Enterprise model,
584
+ # while the new view will to be written to.
585
+ new_entity.suffix = f"{self.view_prefix}{ref_view.view.suffix}"
586
+ new_view = DMSView(
587
+ view=ViewEntity(
588
+ # MyPy we validate that version is string in the constructor
589
+ space=self.new_model_id.space,
590
+ externalId=ref_view.view.external_id,
591
+ version=self.new_model_id.version, # type: ignore[arg-type]
592
+ )
593
+ )
594
+ new_views.append(new_view)
595
+ read_view_by_new_view[new_view.view] = new_entity
596
+ elif self.properties == "repeat":
597
+ # This is a slight misuse of the read_view_by_new_view. For the repeat mode, we only
598
+ # care about the keys in this dictionary. But instead of creating a separate set, we
599
+ # do this.
600
+ read_view_by_new_view[new_entity] = new_entity
601
+
602
+ renaming[ref_view.view] = new_entity
603
+ new_views.append(ref_view.model_copy(deep=True, update={"implements": None, "view": new_entity}))
604
+
605
+ new_properties = SheetList[DMSProperty]()
606
+ for prop in reference.properties:
607
+ if prop.view in skipped_views:
608
+ continue
609
+ new_property = prop.model_copy(deep=True)
610
+ if new_property.value_type in renaming and isinstance(new_property.value_type, ViewEntity):
611
+ new_property.value_type = renaming[new_property.value_type]
612
+ if new_property.view in renaming:
613
+ new_property.view = renaming[new_property.view]
614
+ new_properties.append(new_property)
615
+ return new_views, new_properties, read_view_by_new_view
616
+
617
+ def _create_containers_update_view_filter(
618
+ self, new_views: SheetList[DMSView], reference: DMSRules, read_view_by_new_view: dict[ViewEntity, ViewEntity]
619
+ ) -> tuple[SheetList[DMSContainer], SheetList[DMSProperty]]:
620
+ new_containers = SheetList[DMSContainer]()
621
+ container_properties: SheetList[DMSProperty] = SheetList[DMSProperty]()
622
+ ref_containers_by_ref_view: dict[ViewEntity, set[ContainerEntity]] = defaultdict(set)
623
+ ref_views_by_external_id = {
624
+ view.view.external_id: view for view in reference.views if view.view.space == reference.metadata.space
625
+ }
626
+ if self.filter_type == "container":
627
+ for prop in reference.properties:
628
+ if prop.container:
629
+ ref_containers_by_ref_view[prop.view].add(prop.container)
630
+ read_views = set(read_view_by_new_view.values())
631
+ for view in new_views:
632
+ if view.view in read_view_by_new_view:
633
+ read_view = read_view_by_new_view[view.view]
634
+ container_entity = ContainerEntity(space=self.new_model_id.space, externalId=view.view.external_id)
635
+ prefix = to_camel(view.view.suffix)
636
+ if self.properties == "repeat" and self.dummy_property:
637
+ property_ = DMSProperty(
638
+ view=view.view,
639
+ view_property=f"{prefix}{self.dummy_property}",
640
+ value_type=String(),
641
+ nullable=True,
642
+ immutable=False,
643
+ is_list=False,
644
+ container=container_entity,
645
+ container_property=f"{prefix}{self.dummy_property}",
646
+ )
647
+ new_containers.append(DMSContainer(container=container_entity))
648
+ container_properties.append(property_)
649
+ elif self.properties == "repeat" and self.dummy_property is None:
650
+ # For this case we set the filter. This is used by the DataProductModel.
651
+ # Inherit view filter from original model to ensure the same instances are returned
652
+ # when querying the new view.
653
+ if ref_view := ref_views_by_external_id.get(view.view.external_id):
654
+ self._set_view_filter(view, ref_containers_by_ref_view, ref_view)
655
+ elif self.properties == "connection" and self.direct_property:
656
+ property_ = DMSProperty(
657
+ view=view.view,
658
+ view_property=self.direct_property,
659
+ value_type=read_view,
660
+ nullable=True,
661
+ immutable=False,
662
+ is_list=False,
663
+ container=container_entity,
664
+ container_property=self.direct_property,
665
+ )
666
+ new_containers.append(DMSContainer(container=container_entity))
667
+ container_properties.append(property_)
668
+ else:
669
+ raise NeatValueError(f"Unsupported properties mode: {self.properties}")
670
+
671
+ if self.properties == "connection" and view.view in read_views:
672
+ # Need to ensure that the 'Enterprise' view always returns the same instances
673
+ # as the original view, no matter which properties are removed by the user.
674
+ if ref_view := ref_views_by_external_id.get(view.view.external_id.removeprefix(self.view_prefix)):
675
+ self._set_view_filter(view, ref_containers_by_ref_view, ref_view)
676
+ return new_containers, container_properties
677
+
678
+ def _set_view_filter(
679
+ self, view: DMSView, ref_containers_by_ref_view: dict[ViewEntity, set[ContainerEntity]], ref_view: DMSView
680
+ ) -> None:
681
+ if self.filter_type == "view":
682
+ view.filter_ = HasDataFilter(inner=[ref_view.view])
683
+ elif self.filter_type == "container" and (ref_containers := ref_containers_by_ref_view.get(ref_view.view)):
684
+ # Sorting to ensure deterministic order
685
+ view.filter_ = HasDataFilter(inner=sorted(ref_containers))
686
+
687
+
688
+ class ToDataProductModel(ToSolutionModel):
689
+ """
690
+
691
+ Args:
692
+ new_model_id: DataData model identifier for the new model.
693
+ include: The views to include in the data product data model. Can be either "same-space" or "all".
694
+ filter_type: This is the type of filter to apply to the new views. The filter is used to
695
+ ensure that the new views will return the same instance as the original views. The view filter is the
696
+ simplest filter, but it has limitation in the fusion UI. The container filter is in essence a more
697
+ verbose version of the view filter, and it has better support in the fusion UI. The default is "container".
698
+ """
699
+
700
+ type_: ClassVar[str] = "data_product"
670
701
 
671
- class ReduceCogniteModel(RulesTransformer[DMSRules, DMSRules]):
702
+ def __init__(
703
+ self,
704
+ new_model_id: DataModelIdentifier,
705
+ include: Literal["same-space", "all"] = "same-space",
706
+ filter_type: Literal["container", "view"] = "container",
707
+ skip_cognite_views: bool = True,
708
+ ):
709
+ super().__init__(
710
+ new_model_id,
711
+ properties="repeat",
712
+ dummy_property=None,
713
+ filter_type=filter_type,
714
+ exclude_views_in_other_spaces=include == "same-space",
715
+ skip_cognite_views=skip_cognite_views,
716
+ )
717
+ self.include = include
718
+
719
+ def transform(self, rules: DMSRules) -> DMSRules:
720
+ # Overwrite this to avoid the warning.
721
+ return self._to_solution(rules)
722
+
723
+
724
+ class DropModelViews(VerifiedRulesTransformer[DMSRules, DMSRules]):
672
725
  _ASSET_VIEW = ViewId("cdf_cdm", "CogniteAsset", "v1")
673
726
  _VIEW_BY_COLLECTION: Mapping[Literal["3D", "Annotation", "BaseViews"], frozenset[ViewId]] = {
674
727
  "3D": frozenset(
@@ -707,51 +760,73 @@ class ReduceCogniteModel(RulesTransformer[DMSRules, DMSRules]):
707
760
  ),
708
761
  }
709
762
 
710
- def __init__(self, drop: Collection[Literal["3D", "Annotation", "BaseViews"] | str]):
711
- self.drop_collection = cast(
712
- list[Literal["3D", "Annotation", "BaseViews"]],
713
- [collection for collection in drop if collection in self._VIEW_BY_COLLECTION],
763
+ def __init__(
764
+ self,
765
+ view_external_id: str | SequenceNotStr[str] | None = None,
766
+ group: Literal["3D", "Annotation", "BaseViews"]
767
+ | Collection[Literal["3D", "Annotation", "BaseViews"]]
768
+ | None = None,
769
+ ):
770
+ self.drop_external_ids = (
771
+ {view_external_id} if isinstance(view_external_id, str) else set(view_external_id or [])
772
+ )
773
+ self.drop_collection = (
774
+ [group]
775
+ if isinstance(group, str)
776
+ else cast(
777
+ list[Literal["3D", "Annotation", "BaseViews"]],
778
+ [collection for collection in group or [] if collection in self._VIEW_BY_COLLECTION],
779
+ )
714
780
  )
715
- self.drop_external_ids = {external_id for external_id in drop if external_id not in self._VIEW_BY_COLLECTION}
716
781
 
717
782
  def transform(self, rules: DMSRules) -> DMSRules:
718
- verified = rules
719
- if verified.metadata.as_data_model_id() not in COGNITE_MODELS:
720
- raise NeatValueError(f"Can only reduce Cognite Data Models, not {verified.metadata.as_data_model_id()}")
721
-
722
- exclude_views = {view for collection in self.drop_collection for view in self._VIEW_BY_COLLECTION[collection]}
723
- exclude_views |= {view.view.as_id() for view in verified.views if view.view.suffix in self.drop_external_ids}
724
- new_model = verified.model_copy(deep=True)
783
+ exclude_views: set[ViewEntity] = {
784
+ view.view for view in rules.views if view.view.suffix in self.drop_external_ids
785
+ }
786
+ if rules.metadata.as_data_model_id() in COGNITE_MODELS:
787
+ exclude_views |= {
788
+ ViewEntity.from_id(view_id, "v1")
789
+ for collection in self.drop_collection
790
+ for view_id in self._VIEW_BY_COLLECTION[collection]
791
+ }
792
+ new_model = rules.model_copy(deep=True)
725
793
 
726
794
  properties_by_view = DMSAnalysis(new_model).classes_with_properties(consider_inheritance=True)
727
795
 
728
- new_model.views = SheetList[DMSView](
729
- [view for view in new_model.views if view.view.as_id() not in exclude_views]
730
- )
796
+ new_model.views = SheetList[DMSView]([view for view in new_model.views if view.view not in exclude_views])
731
797
  new_properties = SheetList[DMSProperty]()
732
-
798
+ mapped_containers: set[ContainerEntity] = set()
733
799
  for view in new_model.views:
734
800
  for prop in properties_by_view[view.view]:
735
- if self._is_asset_3D_property(prop):
801
+ if "3D" in self.drop_collection and self._is_asset_3D_property(prop):
736
802
  # We filter out the 3D property of asset
737
803
  continue
804
+ if isinstance(prop.value_type, ViewEntity) and prop.value_type in exclude_views:
805
+ continue
738
806
  new_properties.append(prop)
807
+ if prop.container:
808
+ mapped_containers.add(prop.container)
739
809
 
740
810
  new_model.properties = new_properties
811
+ new_model.containers = (
812
+ SheetList[DMSContainer](
813
+ [container for container in new_model.containers or [] if container.container in mapped_containers]
814
+ )
815
+ or None
816
+ )
741
817
 
742
818
  return new_model
743
819
 
744
- def _is_asset_3D_property(self, prop: DMSProperty) -> bool:
745
- if "3D" not in self.drop_collection:
746
- return False
747
- return prop.view.as_id() == self._ASSET_VIEW and prop.view_property == "object3D"
820
+ @classmethod
821
+ def _is_asset_3D_property(cls, prop: DMSProperty) -> bool:
822
+ return prop.view.as_id() == cls._ASSET_VIEW and prop.view_property == "object3D"
748
823
 
749
824
  @property
750
825
  def description(self) -> str:
751
826
  return f"Removed {len(self.drop_external_ids) + len(self.drop_collection)} views from data model"
752
827
 
753
828
 
754
- class IncludeReferenced(RulesTransformer[DMSRules, DMSRules]):
829
+ class IncludeReferenced(VerifiedRulesTransformer[DMSRules, DMSRules]):
755
830
  def __init__(self, client: NeatClient, include_properties: bool = False) -> None:
756
831
  self._client = client
757
832
  self.include_properties = include_properties
@@ -802,7 +877,7 @@ class IncludeReferenced(RulesTransformer[DMSRules, DMSRules]):
802
877
  return "Included referenced views and containers in the data model."
803
878
 
804
879
 
805
- class AddClassImplements(RulesTransformer[InformationRules, InformationRules]):
880
+ class AddClassImplements(VerifiedRulesTransformer[InformationRules, InformationRules]):
806
881
  def __init__(self, implements: str, suffix: str):
807
882
  self.implements = implements
808
883
  self.suffix = suffix
@@ -820,7 +895,7 @@ class AddClassImplements(RulesTransformer[InformationRules, InformationRules]):
820
895
  return f"Added implements property to classes with suffix {self.suffix}"
821
896
 
822
897
 
823
- class ClassicPrepareCore(RulesTransformer[InformationRules, InformationRules]):
898
+ class ClassicPrepareCore(VerifiedRulesTransformer[InformationRules, InformationRules]):
824
899
  """Update the classic data model with the following:
825
900
 
826
901
  This is a special purpose transformer that is only intended to be used with when reading
@@ -902,7 +977,7 @@ class ClassicPrepareCore(RulesTransformer[InformationRules, InformationRules]):
902
977
  return output
903
978
 
904
979
 
905
- class ChangeViewPrefix(RulesTransformer[DMSRules, DMSRules]):
980
+ class ChangeViewPrefix(VerifiedRulesTransformer[DMSRules, DMSRules]):
906
981
  def __init__(self, old: str, new: str) -> None:
907
982
  self.old = old
908
983
  self.new = new
@@ -927,7 +1002,7 @@ class ChangeViewPrefix(RulesTransformer[DMSRules, DMSRules]):
927
1002
  return output
928
1003
 
929
1004
 
930
- class MergeDMSRules(RulesTransformer[DMSRules, DMSRules]):
1005
+ class MergeDMSRules(VerifiedRulesTransformer[DMSRules, DMSRules]):
931
1006
  def __init__(self, extra: DMSRules) -> None:
932
1007
  self.extra = extra
933
1008
 
@@ -969,7 +1044,7 @@ class MergeDMSRules(RulesTransformer[DMSRules, DMSRules]):
969
1044
  return f"Merged with {self.extra.metadata.as_data_model_id()}"
970
1045
 
971
1046
 
972
- class MergeInformationRules(RulesTransformer[InformationRules, InformationRules]):
1047
+ class MergeInformationRules(VerifiedRulesTransformer[InformationRules, InformationRules]):
973
1048
  def __init__(self, extra: InformationRules) -> None:
974
1049
  self.extra = extra
975
1050
 
@@ -997,7 +1072,7 @@ class _InformationRulesConverter:
997
1072
  self.property_count_by_container: dict[ContainerEntity, int] = defaultdict(int)
998
1073
 
999
1074
  def as_dms_rules(
1000
- self, ignore_undefined_value_types: bool = False, reserved_properties: Literal["error", "skip"] = "error"
1075
+ self, ignore_undefined_value_types: bool = False, reserved_properties: Literal["error", "warning"] = "error"
1001
1076
  ) -> "DMSRules":
1002
1077
  from cognite.neat._rules.models.dms._rules import (
1003
1078
  DMSContainer,