cognite-neat 0.103.1__py3-none-any.whl → 0.105.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.

Files changed (175) hide show
  1. cognite/neat/_client/_api/data_modeling_loaders.py +83 -23
  2. cognite/neat/_client/_api/schema.py +2 -1
  3. cognite/neat/_client/data_classes/neat_sequence.py +261 -0
  4. cognite/neat/_client/data_classes/schema.py +5 -1
  5. cognite/neat/_client/testing.py +33 -0
  6. cognite/neat/_constants.py +56 -0
  7. cognite/neat/_graph/extractors/_classic_cdf/_base.py +6 -5
  8. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +225 -11
  9. cognite/neat/_graph/extractors/_mock_graph_generator.py +2 -2
  10. cognite/neat/_graph/loaders/_rdf2dms.py +13 -2
  11. cognite/neat/_graph/transformers/__init__.py +3 -1
  12. cognite/neat/_graph/transformers/_base.py +109 -1
  13. cognite/neat/_graph/transformers/_classic_cdf.py +6 -1
  14. cognite/neat/_graph/transformers/_prune_graph.py +103 -47
  15. cognite/neat/_graph/transformers/_rdfpath.py +41 -17
  16. cognite/neat/_graph/transformers/_value_type.py +188 -151
  17. cognite/neat/_issues/__init__.py +0 -2
  18. cognite/neat/_issues/_base.py +54 -43
  19. cognite/neat/_issues/warnings/__init__.py +4 -1
  20. cognite/neat/_issues/warnings/_general.py +7 -0
  21. cognite/neat/_issues/warnings/_resources.py +12 -1
  22. cognite/neat/_rules/_shared.py +18 -34
  23. cognite/neat/_rules/exporters/_base.py +28 -2
  24. cognite/neat/_rules/exporters/_rules2dms.py +39 -1
  25. cognite/neat/_rules/exporters/_rules2excel.py +13 -2
  26. cognite/neat/_rules/exporters/_rules2instance_template.py +4 -0
  27. cognite/neat/_rules/exporters/_rules2ontology.py +13 -1
  28. cognite/neat/_rules/exporters/_rules2yaml.py +4 -0
  29. cognite/neat/_rules/importers/_base.py +9 -0
  30. cognite/neat/_rules/importers/_dms2rules.py +80 -57
  31. cognite/neat/_rules/importers/_dtdl2rules/dtdl_importer.py +5 -2
  32. cognite/neat/_rules/importers/_rdf/_base.py +10 -8
  33. cognite/neat/_rules/importers/_rdf/_imf2rules.py +4 -0
  34. cognite/neat/_rules/importers/_rdf/_inference2rules.py +7 -0
  35. cognite/neat/_rules/importers/_rdf/_owl2rules.py +4 -0
  36. cognite/neat/_rules/importers/_spreadsheet2rules.py +17 -8
  37. cognite/neat/_rules/importers/_yaml2rules.py +21 -7
  38. cognite/neat/_rules/models/_base_input.py +1 -1
  39. cognite/neat/_rules/models/_base_rules.py +9 -1
  40. cognite/neat/_rules/models/dms/_rules.py +4 -0
  41. cognite/neat/_rules/models/dms/_rules_input.py +9 -0
  42. cognite/neat/_rules/models/entities/_wrapped.py +10 -5
  43. cognite/neat/_rules/models/information/_rules.py +4 -0
  44. cognite/neat/_rules/models/information/_rules_input.py +9 -0
  45. cognite/neat/_rules/models/mapping/_classic2core.py +2 -5
  46. cognite/neat/_rules/models/mapping/_classic2core.yaml +239 -38
  47. cognite/neat/_rules/transformers/__init__.py +13 -6
  48. cognite/neat/_rules/transformers/_base.py +41 -65
  49. cognite/neat/_rules/transformers/_converters.py +404 -234
  50. cognite/neat/_rules/transformers/_mapping.py +93 -72
  51. cognite/neat/_rules/transformers/_verification.py +50 -38
  52. cognite/neat/_session/_base.py +32 -121
  53. cognite/neat/_session/_inspect.py +5 -3
  54. cognite/neat/_session/_mapping.py +17 -105
  55. cognite/neat/_session/_prepare.py +138 -268
  56. cognite/neat/_session/_read.py +39 -195
  57. cognite/neat/_session/_set.py +6 -30
  58. cognite/neat/_session/_show.py +40 -21
  59. cognite/neat/_session/_state.py +49 -107
  60. cognite/neat/_session/_to.py +44 -33
  61. cognite/neat/_shared.py +23 -2
  62. cognite/neat/_store/_provenance.py +3 -82
  63. cognite/neat/_store/_rules_store.py +368 -10
  64. cognite/neat/_store/exceptions.py +23 -0
  65. cognite/neat/_utils/graph_transformations_report.py +36 -0
  66. cognite/neat/_utils/rdf_.py +8 -0
  67. cognite/neat/_utils/reader/_base.py +27 -0
  68. cognite/neat/_utils/spreadsheet.py +5 -4
  69. cognite/neat/_version.py +1 -1
  70. {cognite_neat-0.103.1.dist-info → cognite_neat-0.105.0.dist-info}/METADATA +3 -2
  71. cognite_neat-0.105.0.dist-info/RECORD +179 -0
  72. {cognite_neat-0.103.1.dist-info → cognite_neat-0.105.0.dist-info}/WHEEL +1 -1
  73. cognite/neat/_app/api/__init__.py +0 -0
  74. cognite/neat/_app/api/asgi/metrics.py +0 -4
  75. cognite/neat/_app/api/configuration.py +0 -98
  76. cognite/neat/_app/api/context_manager/__init__.py +0 -3
  77. cognite/neat/_app/api/context_manager/manager.py +0 -16
  78. cognite/neat/_app/api/data_classes/__init__.py +0 -0
  79. cognite/neat/_app/api/data_classes/rest.py +0 -59
  80. cognite/neat/_app/api/explorer.py +0 -66
  81. cognite/neat/_app/api/routers/configuration.py +0 -25
  82. cognite/neat/_app/api/routers/crud.py +0 -102
  83. cognite/neat/_app/api/routers/metrics.py +0 -10
  84. cognite/neat/_app/api/routers/workflows.py +0 -224
  85. cognite/neat/_app/api/utils/__init__.py +0 -0
  86. cognite/neat/_app/api/utils/data_mapping.py +0 -17
  87. cognite/neat/_app/api/utils/logging.py +0 -26
  88. cognite/neat/_app/api/utils/query_templates.py +0 -92
  89. cognite/neat/_app/main.py +0 -17
  90. cognite/neat/_app/monitoring/__init__.py +0 -0
  91. cognite/neat/_app/monitoring/metrics.py +0 -69
  92. cognite/neat/_app/ui/index.html +0 -1
  93. cognite/neat/_app/ui/neat-app/.gitignore +0 -23
  94. cognite/neat/_app/ui/neat-app/README.md +0 -70
  95. cognite/neat/_app/ui/neat-app/build/asset-manifest.json +0 -14
  96. cognite/neat/_app/ui/neat-app/build/favicon.ico +0 -0
  97. cognite/neat/_app/ui/neat-app/build/img/architect-icon.svg +0 -116
  98. cognite/neat/_app/ui/neat-app/build/img/developer-icon.svg +0 -112
  99. cognite/neat/_app/ui/neat-app/build/img/sme-icon.svg +0 -34
  100. cognite/neat/_app/ui/neat-app/build/index.html +0 -1
  101. cognite/neat/_app/ui/neat-app/build/logo192.png +0 -0
  102. cognite/neat/_app/ui/neat-app/build/manifest.json +0 -25
  103. cognite/neat/_app/ui/neat-app/build/robots.txt +0 -3
  104. cognite/neat/_app/ui/neat-app/build/static/css/main.72e3d92e.css +0 -2
  105. cognite/neat/_app/ui/neat-app/build/static/css/main.72e3d92e.css.map +0 -1
  106. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js +0 -3
  107. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js.LICENSE.txt +0 -88
  108. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js.map +0 -1
  109. cognite/neat/_app/ui/neat-app/build/static/media/logo.8093b84df9ed36a174c629d6fe0b730d.svg +0 -1
  110. cognite/neat/_app/ui/neat-app/package-lock.json +0 -18306
  111. cognite/neat/_app/ui/neat-app/package.json +0 -62
  112. cognite/neat/_app/ui/neat-app/public/favicon.ico +0 -0
  113. cognite/neat/_app/ui/neat-app/public/img/architect-icon.svg +0 -116
  114. cognite/neat/_app/ui/neat-app/public/img/developer-icon.svg +0 -112
  115. cognite/neat/_app/ui/neat-app/public/img/sme-icon.svg +0 -34
  116. cognite/neat/_app/ui/neat-app/public/index.html +0 -43
  117. cognite/neat/_app/ui/neat-app/public/logo192.png +0 -0
  118. cognite/neat/_app/ui/neat-app/public/manifest.json +0 -25
  119. cognite/neat/_app/ui/neat-app/public/robots.txt +0 -3
  120. cognite/neat/_app/ui/neat-app/src/App.css +0 -38
  121. cognite/neat/_app/ui/neat-app/src/App.js +0 -17
  122. cognite/neat/_app/ui/neat-app/src/App.test.js +0 -8
  123. cognite/neat/_app/ui/neat-app/src/MainContainer.tsx +0 -70
  124. cognite/neat/_app/ui/neat-app/src/components/JsonViewer.tsx +0 -43
  125. cognite/neat/_app/ui/neat-app/src/components/LocalUploader.tsx +0 -124
  126. cognite/neat/_app/ui/neat-app/src/components/OverviewComponentEditorDialog.tsx +0 -63
  127. cognite/neat/_app/ui/neat-app/src/components/StepEditorDialog.tsx +0 -511
  128. cognite/neat/_app/ui/neat-app/src/components/TabPanel.tsx +0 -36
  129. cognite/neat/_app/ui/neat-app/src/components/Utils.tsx +0 -56
  130. cognite/neat/_app/ui/neat-app/src/components/WorkflowDeleteDialog.tsx +0 -60
  131. cognite/neat/_app/ui/neat-app/src/components/WorkflowExecutionReport.tsx +0 -112
  132. cognite/neat/_app/ui/neat-app/src/components/WorkflowImportExportDialog.tsx +0 -67
  133. cognite/neat/_app/ui/neat-app/src/components/WorkflowMetadataDialog.tsx +0 -79
  134. cognite/neat/_app/ui/neat-app/src/index.css +0 -13
  135. cognite/neat/_app/ui/neat-app/src/index.js +0 -13
  136. cognite/neat/_app/ui/neat-app/src/logo.svg +0 -1
  137. cognite/neat/_app/ui/neat-app/src/reportWebVitals.js +0 -13
  138. cognite/neat/_app/ui/neat-app/src/setupTests.js +0 -5
  139. cognite/neat/_app/ui/neat-app/src/types/WorkflowTypes.ts +0 -388
  140. cognite/neat/_app/ui/neat-app/src/views/AboutView.tsx +0 -61
  141. cognite/neat/_app/ui/neat-app/src/views/ConfigView.tsx +0 -184
  142. cognite/neat/_app/ui/neat-app/src/views/GlobalConfigView.tsx +0 -180
  143. cognite/neat/_app/ui/neat-app/src/views/WorkflowView.tsx +0 -570
  144. cognite/neat/_app/ui/neat-app/tsconfig.json +0 -27
  145. cognite/neat/_rules/transformers/_pipelines.py +0 -70
  146. cognite/neat/_workflows/__init__.py +0 -17
  147. cognite/neat/_workflows/base.py +0 -590
  148. cognite/neat/_workflows/cdf_store.py +0 -393
  149. cognite/neat/_workflows/examples/Export_DMS/workflow.yaml +0 -89
  150. cognite/neat/_workflows/examples/Export_Semantic_Data_Model/workflow.yaml +0 -66
  151. cognite/neat/_workflows/examples/Import_DMS/workflow.yaml +0 -65
  152. cognite/neat/_workflows/examples/Validate_Rules/workflow.yaml +0 -67
  153. cognite/neat/_workflows/examples/Validate_Solution_Model/workflow.yaml +0 -64
  154. cognite/neat/_workflows/manager.py +0 -292
  155. cognite/neat/_workflows/model.py +0 -203
  156. cognite/neat/_workflows/steps/__init__.py +0 -0
  157. cognite/neat/_workflows/steps/data_contracts.py +0 -109
  158. cognite/neat/_workflows/steps/lib/__init__.py +0 -0
  159. cognite/neat/_workflows/steps/lib/current/__init__.py +0 -6
  160. cognite/neat/_workflows/steps/lib/current/graph_extractor.py +0 -100
  161. cognite/neat/_workflows/steps/lib/current/graph_loader.py +0 -51
  162. cognite/neat/_workflows/steps/lib/current/graph_store.py +0 -48
  163. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +0 -537
  164. cognite/neat/_workflows/steps/lib/current/rules_importer.py +0 -398
  165. cognite/neat/_workflows/steps/lib/current/rules_validator.py +0 -106
  166. cognite/neat/_workflows/steps/lib/io/__init__.py +0 -1
  167. cognite/neat/_workflows/steps/lib/io/io_steps.py +0 -393
  168. cognite/neat/_workflows/steps/step_model.py +0 -79
  169. cognite/neat/_workflows/steps_registry.py +0 -218
  170. cognite/neat/_workflows/tasks.py +0 -18
  171. cognite/neat/_workflows/triggers.py +0 -169
  172. cognite/neat/_workflows/utils.py +0 -19
  173. cognite_neat-0.103.1.dist-info/RECORD +0 -275
  174. {cognite_neat-0.103.1.dist-info → cognite_neat-0.105.0.dist-info}/LICENSE +0 -0
  175. {cognite_neat-0.103.1.dist-info → cognite_neat-0.105.0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,7 @@
1
+ import dataclasses
1
2
  import re
2
3
  import warnings
3
- from abc import ABC, abstractmethod
4
+ from abc import ABC
4
5
  from collections import Counter, defaultdict
5
6
  from collections.abc import Collection, Mapping
6
7
  from datetime import date, datetime
@@ -9,11 +10,12 @@ from typing import ClassVar, Literal, TypeVar, cast, overload
9
10
  from cognite.client.data_classes import data_modeling as dms
10
11
  from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier, ViewId
11
12
 
13
+ from cognite.neat._client import NeatClient
14
+ from cognite.neat._client.data_classes.data_modeling import ContainerApplyDict, ViewApplyDict
12
15
  from cognite.neat._constants import (
13
16
  COGNITE_MODELS,
14
17
  DMS_CONTAINER_PROPERTY_SIZE_LIMIT,
15
18
  )
16
- from cognite.neat._issues._base import IssueList
17
19
  from cognite.neat._issues.errors import NeatValueError
18
20
  from cognite.neat._issues.warnings import NeatValueWarning
19
21
  from cognite.neat._issues.warnings._models import (
@@ -21,13 +23,13 @@ from cognite.neat._issues.warnings._models import (
21
23
  SolutionModelBuildOnTopOfCDMWarning,
22
24
  )
23
25
  from cognite.neat._rules._shared import (
24
- InputRules,
25
- JustRules,
26
- OutRules,
26
+ ReadInputRules,
27
27
  ReadRules,
28
+ T_InputRules,
28
29
  VerifiedRules,
29
30
  )
30
31
  from cognite.neat._rules.analysis import DMSAnalysis
32
+ from cognite.neat._rules.importers import DMSImporter
31
33
  from cognite.neat._rules.models import (
32
34
  DMSInputRules,
33
35
  DMSRules,
@@ -36,7 +38,7 @@ from cognite.neat._rules.models import (
36
38
  data_types,
37
39
  )
38
40
  from cognite.neat._rules.models.data_types import AnyURI, DataType, String
39
- from cognite.neat._rules.models.dms import DMSMetadata, DMSProperty, DMSView
41
+ from cognite.neat._rules.models.dms import DMSMetadata, DMSProperty, DMSValidation, DMSView
40
42
  from cognite.neat._rules.models.dms._rules import DMSContainer
41
43
  from cognite.neat._rules.models.entities import (
42
44
  ClassEntity,
@@ -44,6 +46,7 @@ from cognite.neat._rules.models.entities import (
44
46
  DMSUnknownEntity,
45
47
  EdgeEntity,
46
48
  Entity,
49
+ HasDataFilter,
47
50
  MultiValueTypeInfo,
48
51
  ReverseConnectionEntity,
49
52
  T_Entity,
@@ -56,44 +59,38 @@ from cognite.neat._rules.models.information._rules_input import (
56
59
  InformationInputProperty,
57
60
  InformationInputRules,
58
61
  )
59
- from cognite.neat._utils.collection_ import remove_list_elements
60
62
  from cognite.neat._utils.text import to_camel
61
63
 
62
64
  from ._base import RulesTransformer
65
+ from ._verification import VerifyDMSRules
63
66
 
64
67
  T_VerifiedInRules = TypeVar("T_VerifiedInRules", bound=VerifiedRules)
65
68
  T_VerifiedOutRules = TypeVar("T_VerifiedOutRules", bound=VerifiedRules)
66
- T_InputInRules = TypeVar("T_InputInRules", bound=InputRules)
67
- T_InputOutRules = TypeVar("T_InputOutRules", bound=InputRules)
69
+ T_InputInRules = TypeVar("T_InputInRules", bound=ReadInputRules)
70
+ T_InputOutRules = TypeVar("T_InputOutRules", bound=ReadInputRules)
68
71
 
69
72
 
70
73
  class ConversionTransformer(RulesTransformer[T_VerifiedInRules, T_VerifiedOutRules], ABC):
71
74
  """Base class for all conversion transformers."""
72
75
 
73
- def transform(self, rules: T_VerifiedInRules | OutRules[T_VerifiedInRules]) -> JustRules[T_VerifiedOutRules]:
74
- out = self._transform(self._to_rules(rules))
75
- return JustRules(out)
76
+ ...
76
77
 
77
- @abstractmethod
78
- def _transform(self, rules: T_VerifiedInRules) -> T_VerifiedOutRules:
79
- raise NotImplementedError()
80
78
 
81
-
82
- class ToCompliantEntities(RulesTransformer[InformationInputRules, InformationInputRules]): # type: ignore[misc]
79
+ class ToCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], ReadRules[InformationInputRules]]): # type: ignore[misc]
83
80
  """Converts input rules to rules with compliant entity IDs that match regex patters used
84
81
  by DMS schema components."""
85
82
 
86
- def transform(
87
- self, rules: InformationInputRules | OutRules[InformationInputRules]
88
- ) -> ReadRules[InformationInputRules]:
89
- return ReadRules(self._transform(self._to_rules(rules)), IssueList(), {})
90
-
91
- def _transform(self, rules: InformationInputRules) -> InformationInputRules:
92
- rules.classes = self._fix_classes(rules.classes)
93
- rules.properties = self._fix_properties(rules.properties)
94
- rules.metadata.version += "_dms_compliant"
83
+ @property
84
+ def description(self) -> str:
85
+ return "Ensures externalIDs are compliant with CDF"
95
86
 
96
- return rules
87
+ def transform(self, rules: ReadRules[InformationInputRules]) -> ReadRules[InformationInputRules]:
88
+ if rules.rules is None:
89
+ return rules
90
+ copy: InformationInputRules = dataclasses.replace(rules.rules)
91
+ copy.classes = self._fix_classes(copy.classes)
92
+ copy.properties = self._fix_properties(copy.properties)
93
+ return ReadRules(copy, rules.read_context)
97
94
 
98
95
  @classmethod
99
96
  def _fix_entity(cls, entity: str) -> str:
@@ -179,46 +176,47 @@ class ToCompliantEntities(RulesTransformer[InformationInputRules, InformationInp
179
176
  return fixed_definitions
180
177
 
181
178
 
182
- class PrefixEntities(RulesTransformer[InputRules, InputRules]): # type: ignore[misc]
179
+ class PrefixEntities(RulesTransformer[ReadRules[T_InputRules], ReadRules[T_InputRules]]): # type: ignore[type-var]
183
180
  """Prefixes all entities with a given prefix."""
184
181
 
185
182
  def __init__(self, prefix: str) -> None:
186
183
  self._prefix = prefix
187
184
 
188
- def transform(self, rules: InputRules | OutRules[InputRules]) -> ReadRules[InputRules]:
189
- return ReadRules(self._transform(self._to_rules(rules)), IssueList(), {})
190
-
191
- def _transform(self, rules: InputRules) -> InputRules:
192
- rules.metadata.version += f"_prefixed_{self._prefix}"
185
+ @property
186
+ def description(self) -> str:
187
+ return f"Prefixes all views with {self._prefix!r}"
193
188
 
194
- if isinstance(rules, InformationInputRules):
195
- # Todo Make Not mutate input class
189
+ def transform(self, rules: ReadRules[T_InputRules]) -> ReadRules[T_InputRules]:
190
+ in_ = rules.rules
191
+ if in_ is None:
192
+ return rules
193
+ copy: T_InputRules = dataclasses.replace(in_)
194
+ if isinstance(copy, InformationInputRules):
196
195
  prefixed_by_class: dict[str, str] = {}
197
- for cls in rules.classes:
196
+ for cls in copy.classes:
198
197
  prefixed = str(self._with_prefix(cls.class_))
199
198
  prefixed_by_class[str(cls.class_)] = prefixed
200
199
  cls.class_ = prefixed
201
- for prop in rules.properties:
200
+ for prop in copy.properties:
202
201
  prop.class_ = self._with_prefix(prop.class_)
203
202
  if str(prop.value_type) in prefixed_by_class:
204
203
  prop.value_type = prefixed_by_class[str(prop.value_type)]
205
- return rules
206
- elif isinstance(rules, DMSInputRules):
207
- # Todo not mutate input class new_dms = copy.deepcopy(rules)
204
+ return ReadRules(copy, rules.read_context) # type: ignore[arg-type]
205
+ elif isinstance(copy, DMSInputRules):
208
206
  prefixed_by_view: dict[str, str] = {}
209
- for view in rules.views:
207
+ for view in copy.views:
210
208
  prefixed = str(self._with_prefix(view.view))
211
209
  prefixed_by_view[str(view.view)] = prefixed
212
210
  view.view = prefixed
213
- for dms_prop in rules.properties:
211
+ for dms_prop in copy.properties:
214
212
  dms_prop.view = self._with_prefix(dms_prop.view)
215
213
  if str(dms_prop.value_type) in prefixed_by_view:
216
214
  dms_prop.value_type = prefixed_by_view[str(dms_prop.value_type)]
217
- if rules.containers:
218
- for container in rules.containers:
215
+ if copy.containers:
216
+ for container in copy.containers:
219
217
  container.container = self._with_prefix(container.container)
220
- return rules
221
- raise NeatValueError(f"Unsupported rules type: {type(rules)}")
218
+ return ReadRules(copy, rules.read_context)
219
+ raise NeatValueError(f"Unsupported rules type: {type(copy)}")
222
220
 
223
221
  @overload
224
222
  def _with_prefix(self, raw: str) -> str: ...
@@ -254,14 +252,14 @@ class InformationToDMS(ConversionTransformer[InformationRules, DMSRules]):
254
252
  self.ignore_undefined_value_types = ignore_undefined_value_types
255
253
  self.mode = mode
256
254
 
257
- def _transform(self, rules: InformationRules) -> DMSRules:
255
+ def transform(self, rules: InformationRules) -> DMSRules:
258
256
  return _InformationRulesConverter(rules).as_dms_rules(self.ignore_undefined_value_types, self.mode)
259
257
 
260
258
 
261
259
  class DMSToInformation(ConversionTransformer[DMSRules, InformationRules]):
262
260
  """Converts DMSRules to InformationRules."""
263
261
 
264
- def _transform(self, rules: DMSRules) -> InformationRules:
262
+ def transform(self, rules: DMSRules) -> InformationRules:
265
263
  return _DMSRulesConverter(rules).as_information_rules()
266
264
 
267
265
 
@@ -271,13 +269,13 @@ class ConvertToRules(ConversionTransformer[VerifiedRules, VerifiedRules]):
271
269
  def __init__(self, out_cls: type[VerifiedRules]):
272
270
  self._out_cls = out_cls
273
271
 
274
- def _transform(self, rules: VerifiedRules) -> VerifiedRules:
272
+ def transform(self, rules: VerifiedRules) -> VerifiedRules:
275
273
  if isinstance(rules, self._out_cls):
276
274
  return rules
277
275
  if isinstance(rules, InformationRules) and self._out_cls is DMSRules:
278
- return InformationToDMS().transform(rules).rules
276
+ return InformationToDMS().transform(rules)
279
277
  if isinstance(rules, DMSRules) and self._out_cls is InformationRules:
280
- return DMSToInformation().transform(rules).rules
278
+ return DMSToInformation().transform(rules)
281
279
  raise ValueError(f"Unsupported conversion from {type(rules)} to {self._out_cls}")
282
280
 
283
281
 
@@ -288,119 +286,275 @@ class SetIDDMSModel(RulesTransformer[DMSRules, DMSRules]):
288
286
  def __init__(self, new_id: DataModelId | tuple[str, str, str]):
289
287
  self.new_id = DataModelId.load(new_id)
290
288
 
291
- def transform(self, rules: DMSRules | OutRules[DMSRules]) -> JustRules[DMSRules]:
289
+ @property
290
+ def description(self) -> str:
291
+ return f"Sets the Data Model ID to {self.new_id.as_tuple()}"
292
+
293
+ def transform(self, rules: DMSRules) -> DMSRules:
292
294
  if self.new_id.version is None:
293
295
  raise NeatValueError("Version is required when setting a new Data Model ID")
294
- dump = self._to_rules(rules).dump()
296
+ dump = rules.dump()
295
297
  dump["metadata"]["space"] = self.new_id.space
296
298
  dump["metadata"]["external_id"] = self.new_id.external_id
297
299
  dump["metadata"]["version"] = self.new_id.version
298
300
  # Serialize and deserialize to set the new space and external_id
299
301
  # as the default values for the new model.
300
- return JustRules(DMSRules.model_validate(DMSInputRules.load(dump).dump()))
302
+ return DMSRules.model_validate(DMSInputRules.load(dump).dump())
303
+
304
+
305
+ class ToExtensionModel(RulesTransformer[DMSRules, DMSRules], ABC):
306
+ type_: ClassVar[str]
307
+
308
+ def __init__(self, new_model_id: DataModelIdentifier, org_name: str, dummy_property: str | None = None) -> None:
309
+ self.new_model_id = DataModelId.load(new_model_id)
310
+ if not self.new_model_id.version:
311
+ raise NeatValueError("Version is required for the new model.")
312
+ self.org_name = org_name
313
+ self.dummy_property = dummy_property
314
+
315
+ @property
316
+ def description(self) -> str:
317
+ return f"Prepared data model {self.new_model_id} to be {self.type_.replace('_', ' ')} data model."
318
+
319
+ def _create_new_views(
320
+ self, rules: DMSRules
321
+ ) -> tuple[SheetList[DMSView], SheetList[DMSContainer], SheetList[DMSProperty]]:
322
+ """Creates new views for the new model.
323
+
324
+ If the dummy property is provided, it will also create a new container for each view
325
+ with a single property that is the dummy property.
326
+ """
327
+ new_views = SheetList[DMSView]()
328
+ new_containers = SheetList[DMSContainer]()
329
+ new_properties = SheetList[DMSProperty]()
330
+
331
+ for definition in rules.views:
332
+ view_entity = self._remove_cognite_affix(definition.view)
333
+ view_entity.version = cast(str, self.new_model_id.version)
334
+ view_entity.prefix = self.new_model_id.space
335
+
336
+ new_views.append(
337
+ DMSView(
338
+ view=view_entity,
339
+ implements=[definition.view],
340
+ in_model=True,
341
+ name=definition.name,
342
+ )
343
+ )
344
+ if self.dummy_property is None:
345
+ continue
346
+
347
+ container_entity = ContainerEntity(space=view_entity.prefix, externalId=view_entity.external_id)
301
348
 
349
+ container = DMSContainer(container=container_entity)
350
+
351
+ prefix = to_camel(view_entity.suffix)
352
+ property_ = DMSProperty(
353
+ view=view_entity,
354
+ view_property=f"{prefix}{self.dummy_property}",
355
+ value_type=String(),
356
+ nullable=True,
357
+ immutable=False,
358
+ is_list=False,
359
+ container=container_entity,
360
+ container_property=f"{prefix}{self.dummy_property}",
361
+ )
362
+
363
+ new_properties.append(property_)
364
+ new_containers.append(container)
365
+
366
+ return new_views, new_containers, new_properties
367
+
368
+ def _remove_cognite_affix(self, entity: _T_Entity) -> _T_Entity:
369
+ """This method removes `Cognite` affix from the entity."""
370
+ new_suffix = entity.suffix.replace("Cognite", self.org_name or "")
371
+ if isinstance(entity, ViewEntity):
372
+ return ViewEntity(space=entity.space, externalId=new_suffix, version=entity.version) # type: ignore[return-value]
373
+ elif isinstance(entity, ClassEntity):
374
+ return ClassEntity(prefix=entity.prefix, suffix=new_suffix, version=entity.version) # type: ignore[return-value]
375
+ raise ValueError(f"Unsupported entity type: {type(entity)}")
376
+
377
+
378
+ class ToEnterpriseModel(ToExtensionModel):
379
+ type_: ClassVar[str] = "enterprise"
302
380
 
303
- class ToExtension(RulesTransformer[DMSRules, DMSRules]):
304
381
  def __init__(
305
382
  self,
306
383
  new_model_id: DataModelIdentifier,
307
384
  org_name: str = "My",
308
- type_: Literal["enterprise", "solution", "data_product"] = "enterprise",
309
- mode: Literal["read", "write"] = "read",
310
385
  dummy_property: str = "GUID",
311
386
  move_connections: bool = False,
312
- include: Literal["same-space", "all"] = "same-space",
313
387
  ):
314
- self.new_model_id = DataModelId.load(new_model_id)
315
- if not self.new_model_id.version:
316
- raise NeatValueError("Version is required for the new model.")
388
+ super().__init__(new_model_id, org_name, dummy_property)
389
+ self.move_connections = move_connections
317
390
 
318
- self.org_name = org_name
391
+ def transform(self, rules: DMSRules) -> DMSRules:
392
+ reference_model_id = rules.metadata.as_data_model_id()
393
+ if reference_model_id not in COGNITE_MODELS:
394
+ warnings.warn(
395
+ EnterpriseModelNotBuildOnTopOfCDMWarning(reference_model_id=reference_model_id).as_message(),
396
+ stacklevel=2,
397
+ )
398
+
399
+ return self._to_enterprise(rules)
400
+
401
+ def _to_enterprise(self, reference_model: DMSRules) -> DMSRules:
402
+ dump = reference_model.dump()
403
+
404
+ # This will create reference model components in the enterprise model space
405
+ enterprise_model = DMSRules.model_validate(DMSInputRules.load(dump).dump())
406
+
407
+ # Post validation metadata update:
408
+ enterprise_model.metadata.name = self.type_
409
+ enterprise_model.metadata.name = f"{self.org_name} {self.type_} data model"
410
+ enterprise_model.metadata.space = self.new_model_id.space
411
+ enterprise_model.metadata.external_id = self.new_model_id.external_id
412
+ enterprise_model.metadata.version = cast(str, self.new_model_id.version)
413
+
414
+ # Here we are creating enterprise views with a single container with a dummy property
415
+ # for each view
416
+ enterprise_views, enterprise_containers, enterprise_properties = self._create_new_views(enterprise_model)
417
+
418
+ # We keep the reference views, and adding new enterprise views...
419
+ enterprise_model.views.extend(enterprise_views)
420
+
421
+ if self.move_connections:
422
+ # Move connections from reference model to new enterprise model
423
+ enterprise_properties.extend(self._move_connections(enterprise_model))
424
+
425
+ # ... however, we do not want to keep the reference containers and properties
426
+ enterprise_model.containers = enterprise_containers
427
+ enterprise_model.properties = enterprise_properties
428
+
429
+ return enterprise_model
430
+
431
+ @staticmethod
432
+ def _move_connections(rules: DMSRules) -> SheetList[DMSProperty]:
433
+ implements: dict[ViewEntity, list[ViewEntity]] = defaultdict(list)
434
+ new_properties = SheetList[DMSProperty]()
435
+
436
+ for view in rules.views:
437
+ if view.view.space == rules.metadata.space and view.implements:
438
+ for implemented_view in view.implements:
439
+ implements.setdefault(implemented_view, []).append(view.view)
440
+
441
+ # currently only supporting single implementation of reference view in enterprise view
442
+ # connections that do not have properties
443
+ if all(len(v) == 1 for v in implements.values()):
444
+ for prop_ in rules.properties:
445
+ if (
446
+ prop_.view.space != rules.metadata.space
447
+ and prop_.connection
448
+ and isinstance(prop_.value_type, ViewEntity)
449
+ and implements.get(prop_.view)
450
+ and implements.get(prop_.value_type)
451
+ ):
452
+ if isinstance(prop_.connection, EdgeEntity) and prop_.connection.properties:
453
+ continue
454
+ new_property = prop_.model_copy(deep=True)
455
+ new_property.view = implements[prop_.view][0]
456
+ new_property.value_type = implements[prop_.value_type][0]
457
+ new_properties.append(new_property)
458
+
459
+ return new_properties
460
+
461
+
462
+ class ToSolutionModel(ToExtensionModel):
463
+ """Creates a solution data model based on an existing data model.
464
+
465
+ The solution data model will create a new view for each view in the existing model.
466
+
467
+ Args:
468
+ new_model_id: DataData model identifier for the new model.
469
+ org_name: If the existing model is a Cognite Data Model, this will replace the "Cognite" affix.
470
+ mode: The mode of the solution model. Either "read" or "write". A "write" model will create a new
471
+ container for each view with a dummy property. Read mode will only inherit the view filter from the
472
+ original model.
473
+ dummy_property: Only applicable if mode='write'. The identifier of the dummy property in the newly created
474
+ container.
475
+ exclude_views_in_other_spaces: Whether to exclude views that are not in the same space as the existing model,
476
+ when creating the solution model.
477
+ filter_type: If mode="read", this is the type of filter to apply to the new views. The filter is used to
478
+ ensure that the new views will return the same instance as the original views. The view filter is the
479
+ simplest filter, but it has limitation in the fusion UI. The container filter is in essence a more
480
+ verbose version of the view filter, and it has better support in the fusion UI. The default is "container".
481
+
482
+ """
483
+
484
+ type_: ClassVar[str] = "solution"
485
+
486
+ def __init__(
487
+ self,
488
+ new_model_id: DataModelIdentifier,
489
+ org_name: str = "My",
490
+ mode: Literal["read", "write"] = "read",
491
+ dummy_property: str | None = "GUID",
492
+ exclude_views_in_other_spaces: bool = True,
493
+ filter_type: Literal["container", "view"] = "container",
494
+ ):
495
+ super().__init__(new_model_id, org_name, dummy_property if mode == "write" else None)
319
496
  self.mode = mode
320
- self.type_ = type_
321
- self.dummy_property = dummy_property
322
- self.move_connections = move_connections
323
- self.include = include
497
+ self.exclude_views_in_other_spaces = exclude_views_in_other_spaces
498
+ self.filter_type = filter_type
324
499
 
325
- def transform(self, rules: DMSRules | OutRules[DMSRules]) -> JustRules[DMSRules]:
326
- # Copy to ensure immutability
327
- reference_model = self._to_rules(rules)
500
+ def transform(self, rules: DMSRules) -> DMSRules:
501
+ reference_model = rules
328
502
  reference_model_id = reference_model.metadata.as_data_model_id()
329
503
 
330
504
  # if model is solution then we need to get correct space for views and containers
331
- if self.type_ == "solution":
332
- if self.mode not in ["read", "write"]:
333
- raise NeatValueError(f"Unsupported mode: {self.mode}")
334
-
335
- if reference_model_id in COGNITE_MODELS:
336
- warnings.warn(
337
- SolutionModelBuildOnTopOfCDMWarning(reference_model_id=reference_model_id),
338
- stacklevel=2,
339
- )
340
-
341
- return self._to_solution(reference_model)
505
+ if self.mode not in ["read", "write"]:
506
+ raise NeatValueError(f"Unsupported mode: {self.mode}")
342
507
 
343
- elif self.type_ == "enterprise":
344
- if reference_model_id not in COGNITE_MODELS:
345
- warnings.warn(
346
- EnterpriseModelNotBuildOnTopOfCDMWarning(reference_model_id=reference_model_id).as_message(),
347
- stacklevel=2,
348
- )
349
-
350
- return self._to_enterprise(reference_model)
351
- elif self.type_ == "data_product":
352
- expanded = self._expand_properties(reference_model.model_copy(deep=True))
353
- if self.include == "same-space":
354
- expanded.properties = SheetList[DMSProperty](
355
- [prop for prop in expanded.properties if prop.view.space == expanded.metadata.space]
356
- )
357
- expanded.views = SheetList[DMSView](
358
- [view for view in expanded.views if view.view.space == expanded.metadata.space]
359
- )
360
- return self._to_solution(expanded, remove_views_in_other_space=False)
508
+ if reference_model_id in COGNITE_MODELS:
509
+ warnings.warn(
510
+ SolutionModelBuildOnTopOfCDMWarning(reference_model_id=reference_model_id),
511
+ stacklevel=2,
512
+ )
361
513
 
362
- else:
363
- raise NeatValueError(f"Unsupported data model type: {self.type_}")
514
+ return self._to_solution(reference_model)
364
515
 
365
- def _has_views_in_multiple_space(self, rules: DMSRules) -> bool:
516
+ @staticmethod
517
+ def _has_views_in_multiple_space(rules: DMSRules) -> bool:
366
518
  return any(view.view.space != rules.metadata.space for view in rules.views)
367
519
 
368
- def _to_solution(self, reference_rules: DMSRules, remove_views_in_other_space: bool = True) -> JustRules[DMSRules]:
520
+ def _to_solution(self, reference_rules: DMSRules) -> DMSRules:
369
521
  """For creation of solution data model / rules specifically for mapping over existing containers."""
370
522
 
371
- dump = reference_rules.dump()
523
+ dump = reference_rules.dump(entities_exclude_defaults=True)
372
524
 
373
525
  # Prepare new model metadata prior validation
526
+ # Since we dropped the defaults, all entities will update the space and version
527
+ # to the new model space and version
374
528
  dump["metadata"]["name"] = f"{self.org_name} {self.type_} data model"
375
529
  dump["metadata"]["space"] = self.new_model_id.space
376
530
  dump["metadata"]["external_id"] = self.new_model_id.external_id
377
531
  dump["metadata"]["version"] = self.new_model_id.version
378
532
 
379
- # Set implement to NONE for all views
380
- for view in dump["views"]:
381
- view["implements"] = None
382
-
383
- if remove_views_in_other_space and self._has_views_in_multiple_space(reference_rules):
384
- views_to_remove = []
385
- for view in dump["views"]:
386
- if ":" in view["view"]:
387
- views_to_remove.append(view)
388
-
389
- dump["views"] = remove_list_elements(dump["views"], views_to_remove)
390
-
391
533
  solution_model = DMSRules.model_validate(DMSInputRules.load(dump).dump())
392
534
 
393
- # Dropping containers coming from reference model
394
- solution_model.containers = None
395
-
396
- # We want to map properties to existing containers allowing extension
535
+ # This is not desirable for the containers, so we manually fix that here.
536
+ # It is easier to change the space for all entities and then revert the containers, than
537
+ # to change the space for all entities except the containers.
397
538
  for prop in solution_model.properties:
398
539
  if prop.container and prop.container.space == self.new_model_id.space:
540
+ # If the container is in the new model space, we want to map it to the reference model space
541
+ # This is reverting the .dump() -> .load() above.
399
542
  prop.container = ContainerEntity(
400
543
  space=reference_rules.metadata.space,
401
544
  externalId=prop.container.suffix,
402
545
  )
403
546
 
547
+ for view in solution_model.views:
548
+ view.implements = None
549
+
550
+ if self.exclude_views_in_other_spaces and self._has_views_in_multiple_space(reference_rules):
551
+ solution_model.views = SheetList[DMSView](
552
+ [view for view in solution_model.views if view.view.space == solution_model.metadata.space]
553
+ )
554
+
555
+ # Dropping containers coming from reference model
556
+ solution_model.containers = None
557
+
404
558
  # If reference model on which we are mapping one of Cognite Data Models
405
559
  # since we want to affix these with the organization name
406
560
  if reference_rules.metadata.as_data_model_id() in COGNITE_MODELS:
@@ -411,50 +565,73 @@ class ToExtension(RulesTransformer[DMSRules, DMSRules]):
411
565
  prop.value_type = self._remove_cognite_affix(prop.value_type)
412
566
  for view in solution_model.views:
413
567
  view.view = self._remove_cognite_affix(view.view)
568
+ if view.implements:
569
+ view.implements = [self._remove_cognite_affix(implemented) for implemented in view.implements]
414
570
 
415
571
  if self.mode == "write":
416
- _, new_containers, new_properties = self._get_new_components(solution_model)
417
-
572
+ _, new_containers, new_properties = self._create_new_views(solution_model)
418
573
  # Here we add ONLY dummy properties of the solution model and
419
574
  # corresponding solution model space containers to hold them
420
575
  solution_model.containers = new_containers
421
576
  solution_model.properties.extend(new_properties)
577
+ elif self.mode == "read":
578
+ # Inherit view filter from original model to ensure the same instances are returned
579
+ # when querying the new view.
580
+ ref_views_by_external_id = {
581
+ view.view.external_id: view
582
+ for view in reference_rules.views
583
+ if view.view.space == reference_rules.metadata.space
584
+ }
585
+ ref_containers_by_ref_view = defaultdict(set)
586
+ for prop in reference_rules.properties:
587
+ if prop.container:
588
+ ref_containers_by_ref_view[prop.view].add(prop.container)
589
+ for view in solution_model.views:
590
+ if ref_view := ref_views_by_external_id.get(view.view.external_id):
591
+ if self.filter_type == "view":
592
+ view.filter_ = HasDataFilter(inner=[ref_view.view])
593
+ elif self.filter_type == "container" and (
594
+ ref_containers := ref_containers_by_ref_view.get(ref_view.view)
595
+ ):
596
+ # Sorting to ensure deterministic order
597
+ view.filter_ = HasDataFilter(inner=sorted(ref_containers))
422
598
 
423
- return JustRules(solution_model)
424
-
425
- def _to_enterprise(self, reference_model: DMSRules) -> JustRules[DMSRules]:
426
- dump = reference_model.dump()
427
-
428
- # This will create reference model components in the enterprise model space
429
- enterprise_model = DMSRules.model_validate(DMSInputRules.load(dump).dump())
430
-
431
- # Post validation metadata update:
432
- enterprise_model.metadata.name = self.type_
433
- enterprise_model.metadata.name = f"{self.org_name} {self.type_} data model"
434
- enterprise_model.metadata.space = self.new_model_id.space
435
- enterprise_model.metadata.external_id = self.new_model_id.external_id
436
- enterprise_model.metadata.version = cast(str, self.new_model_id.version)
437
-
438
- # Here we are creating enterprise specific components
439
- enterprise_views, enterprise_containers, enterprise_properties = self._get_new_components(enterprise_model)
599
+ return solution_model
440
600
 
441
- # And we are adding them to the enterprise model
442
- # extending reference views with new ones
443
- enterprise_model.views.extend(enterprise_views)
444
601
 
445
- # Move connections from reference model to enterprise model
446
- if self.move_connections:
447
- enterprise_connections = self._move_connections(enterprise_model)
448
- else:
449
- enterprise_connections = SheetList[DMSProperty]()
602
+ class ToDataProductModel(ToSolutionModel):
603
+ type_: ClassVar[str] = "data_product"
450
604
 
451
- # while overwriting containers and properties with new ones
452
- enterprise_model.containers = enterprise_containers
453
- enterprise_model.properties = enterprise_properties
605
+ def __init__(
606
+ self,
607
+ new_model_id: DataModelIdentifier,
608
+ org_name: str = "My",
609
+ include: Literal["same-space", "all"] = "same-space",
610
+ ):
611
+ super().__init__(new_model_id, org_name, mode="read", dummy_property=None, exclude_views_in_other_spaces=False)
612
+ self.include = include
454
613
 
455
- enterprise_properties.extend(enterprise_connections)
614
+ def transform(self, rules: DMSRules) -> DMSRules:
615
+ # Copy to ensure immutability
616
+ expanded = self._expand_properties(rules.model_copy(deep=True))
617
+ if self.include == "same-space":
618
+ expanded.views = SheetList[DMSView](
619
+ [view for view in expanded.views if view.view.space == expanded.metadata.space]
620
+ )
621
+ used_view_entities = {view.view for view in expanded.views}
622
+ expanded.properties = SheetList[DMSProperty](
623
+ [
624
+ prop
625
+ for prop in expanded.properties
626
+ if prop.view.space == expanded.metadata.space
627
+ and (
628
+ (isinstance(prop.value_type, ViewEntity) and prop.value_type in used_view_entities)
629
+ or not isinstance(prop.value_type, ViewEntity)
630
+ )
631
+ ]
632
+ )
456
633
 
457
- return JustRules(enterprise_model)
634
+ return self._to_solution(expanded)
458
635
 
459
636
  @staticmethod
460
637
  def _expand_properties(rules: DMSRules) -> DMSRules:
@@ -478,86 +655,6 @@ class ToExtension(RulesTransformer[DMSRules, DMSRules]):
478
655
  property_ids.add(prop.view_property)
479
656
  return rules
480
657
 
481
- def _remove_cognite_affix(self, entity: _T_Entity) -> _T_Entity:
482
- """This method removes `Cognite` affix from the entity."""
483
- new_suffix = entity.suffix.replace("Cognite", self.org_name or "")
484
- if isinstance(entity, ViewEntity):
485
- return ViewEntity(space=entity.space, externalId=new_suffix, version=entity.version) # type: ignore[return-value]
486
- elif isinstance(entity, ClassEntity):
487
- return ClassEntity(prefix=entity.prefix, suffix=new_suffix, version=entity.version) # type: ignore[return-value]
488
- raise ValueError(f"Unsupported entity type: {type(entity)}")
489
-
490
- def _get_new_components(
491
- self, rules: DMSRules
492
- ) -> tuple[SheetList[DMSView], SheetList[DMSContainer], SheetList[DMSProperty]]:
493
- new_views = SheetList[DMSView]()
494
- new_containers = SheetList[DMSContainer]()
495
- new_properties = SheetList[DMSProperty]()
496
-
497
- for definition in rules.views:
498
- view_entity = self._remove_cognite_affix(definition.view)
499
-
500
- view_entity.version = cast(str, self.new_model_id.version)
501
- view_entity.prefix = self.new_model_id.space
502
- container_entity = ContainerEntity(space=view_entity.prefix, externalId=view_entity.external_id)
503
-
504
- view = DMSView(
505
- view=view_entity,
506
- implements=[definition.view],
507
- in_model=True,
508
- name=definition.name,
509
- )
510
-
511
- container = DMSContainer(
512
- container=container_entity,
513
- )
514
-
515
- property_ = DMSProperty(
516
- view=view_entity,
517
- view_property=f"{to_camel(view_entity.suffix)}{self.dummy_property}",
518
- value_type=String(),
519
- nullable=True,
520
- immutable=False,
521
- is_list=False,
522
- container=container_entity,
523
- container_property=f"{to_camel(view_entity.suffix)}{self.dummy_property}",
524
- )
525
-
526
- new_properties.append(property_)
527
- new_views.append(view)
528
- new_containers.append(container)
529
-
530
- return new_views, new_containers, new_properties
531
-
532
- def _move_connections(self, rules: DMSRules) -> SheetList[DMSProperty]:
533
- implements: dict[ViewEntity, list[ViewEntity]] = defaultdict(list)
534
- new_properties = SheetList[DMSProperty]()
535
-
536
- for view in rules.views:
537
- if view.view.space == rules.metadata.space and view.implements:
538
- for implemented_view in view.implements:
539
- implements.setdefault(implemented_view, []).append(view.view)
540
-
541
- # currently only supporting single implementation of reference view in enterprise view
542
- # connections that do not have properties
543
- if all(len(v) == 1 for v in implements.values()):
544
- for prop_ in rules.properties:
545
- if (
546
- prop_.view.space != rules.metadata.space
547
- and prop_.connection
548
- and isinstance(prop_.value_type, ViewEntity)
549
- and implements.get(prop_.view)
550
- and implements.get(prop_.value_type)
551
- ):
552
- if isinstance(prop_.connection, EdgeEntity) and prop_.connection.properties:
553
- continue
554
- new_property = prop_.model_copy(deep=True)
555
- new_property.view = implements[prop_.view][0]
556
- new_property.value_type = implements[prop_.value_type][0]
557
- new_properties.append(new_property)
558
-
559
- return new_properties
560
-
561
658
 
562
659
  class ReduceCogniteModel(RulesTransformer[DMSRules, DMSRules]):
563
660
  _ASSET_VIEW = ViewId("cdf_cdm", "CogniteAsset", "v1")
@@ -605,8 +702,8 @@ class ReduceCogniteModel(RulesTransformer[DMSRules, DMSRules]):
605
702
  )
606
703
  self.drop_external_ids = {external_id for external_id in drop if external_id not in self._VIEW_BY_COLLECTION}
607
704
 
608
- def transform(self, rules: DMSRules | OutRules[DMSRules]) -> JustRules[DMSRules]:
609
- verified = self._to_rules(rules)
705
+ def transform(self, rules: DMSRules) -> DMSRules:
706
+ verified = rules
610
707
  if verified.metadata.as_data_model_id() not in COGNITE_MODELS:
611
708
  raise NeatValueError(f"Can only reduce Cognite Data Models, not {verified.metadata.as_data_model_id()}")
612
709
 
@@ -630,13 +727,86 @@ class ReduceCogniteModel(RulesTransformer[DMSRules, DMSRules]):
630
727
 
631
728
  new_model.properties = new_properties
632
729
 
633
- return JustRules(new_model)
730
+ return new_model
634
731
 
635
732
  def _is_asset_3D_property(self, prop: DMSProperty) -> bool:
636
733
  if "3D" not in self.drop_collection:
637
734
  return False
638
735
  return prop.view.as_id() == self._ASSET_VIEW and prop.view_property == "object3D"
639
736
 
737
+ @property
738
+ def description(self) -> str:
739
+ return f"Removed {len(self.drop_external_ids) + len(self.drop_collection)} views from data model"
740
+
741
+
742
+ class IncludeReferenced(RulesTransformer[DMSRules, DMSRules]):
743
+ def __init__(self, client: NeatClient, include_properties: bool = False) -> None:
744
+ self._client = client
745
+ self.include_properties = include_properties
746
+
747
+ def transform(self, rules: DMSRules) -> DMSRules:
748
+ dms_rules = rules
749
+ view_ids, container_ids = DMSValidation(dms_rules).imported_views_and_containers_ids()
750
+ if not (view_ids or container_ids):
751
+ warnings.warn(
752
+ NeatValueWarning(
753
+ f"Data model {dms_rules.metadata.as_data_model_id()} does not have any "
754
+ "referenced views or containers."
755
+ "that is not already included in the data model."
756
+ ),
757
+ stacklevel=2,
758
+ )
759
+ return dms_rules
760
+
761
+ schema = self._client.schema.retrieve([v.as_id() for v in view_ids], [c.as_id() for c in container_ids])
762
+ copy_ = dms_rules.model_copy(deep=True)
763
+ # Sorting to ensure deterministic order
764
+ schema.containers = ContainerApplyDict(sorted(schema.containers.items(), key=lambda x: x[0].as_tuple()))
765
+ schema.views = ViewApplyDict(sorted(schema.views.items(), key=lambda x: x[0].as_tuple()))
766
+ importer = DMSImporter(schema)
767
+
768
+ imported = importer.to_rules()
769
+ if imported.rules is None:
770
+ raise NeatValueError("Could not import the referenced views and containers.")
771
+
772
+ verified = VerifyDMSRules(validate=False).transform(imported)
773
+ if copy_.containers is None:
774
+ copy_.containers = verified.containers
775
+ else:
776
+ existing_containers = {c.container for c in copy_.containers}
777
+ copy_.containers.extend([c for c in verified.containers or [] if c.container not in existing_containers])
778
+ existing_views = {v.view for v in copy_.views}
779
+ copy_.views.extend([v for v in verified.views if v.view not in existing_views])
780
+ if self.include_properties:
781
+ existing_properties = {(p.view, p.view_property) for p in copy_.properties}
782
+ copy_.properties.extend(
783
+ [p for p in verified.properties if (p.view, p.view_property) not in existing_properties]
784
+ )
785
+
786
+ return copy_
787
+
788
+ @property
789
+ def description(self) -> str:
790
+ return "Included referenced views and containers in the data model."
791
+
792
+
793
+ class AddClassImplements(RulesTransformer[InformationRules, InformationRules]):
794
+ def __init__(self, implements: str, suffix: str):
795
+ self.implements = implements
796
+ self.suffix = suffix
797
+
798
+ def transform(self, rules: InformationRules) -> InformationRules:
799
+ info_rules = rules
800
+ output = info_rules.model_copy(deep=True)
801
+ for class_ in output.classes:
802
+ if class_.class_.suffix.endswith(self.suffix):
803
+ class_.implements = [ClassEntity(prefix=class_.class_.prefix, suffix=self.implements)]
804
+ return output
805
+
806
+ @property
807
+ def description(self) -> str:
808
+ return f"Added implements property to classes with suffix {self.suffix}"
809
+
640
810
 
641
811
  class _InformationRulesConverter:
642
812
  _edge_properties: ClassVar[frozenset[str]] = frozenset({"endNode", "end_node", "startNode", "start_node"})