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

cognite/neat/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.76.3"
1
+ __version__ = "0.77.1"
@@ -5,7 +5,14 @@ from typing import Literal, TypeAlias, cast
5
5
 
6
6
  from cognite.client import CogniteClient
7
7
  from cognite.client.data_classes._base import CogniteResource, CogniteResourceList
8
- from cognite.client.data_classes.data_modeling import DataModelApply, DataModelId
8
+ from cognite.client.data_classes.data_modeling import (
9
+ ContainerApplyList,
10
+ DataModelApply,
11
+ DataModelApplyList,
12
+ DataModelId,
13
+ SpaceApplyList,
14
+ ViewApplyList,
15
+ )
9
16
  from cognite.client.exceptions import CogniteAPIError
10
17
 
11
18
  from cognite.neat.rules import issues
@@ -114,9 +121,7 @@ class DMSExporter(CDFExporter[DMSSchema]):
114
121
  dms_rules = rules.as_dms_architect_rules()
115
122
  else:
116
123
  raise ValueError(f"{type(rules).__name__} cannot be exported to DMS")
117
- return dms_rules.as_schema(
118
- include_ref=True, include_pipeline=self.export_pipeline, instance_space=self.instance_space
119
- )
124
+ return dms_rules.as_schema(include_pipeline=self.export_pipeline, instance_space=self.instance_space)
120
125
 
121
126
  def delete_from_cdf(self, rules: Rules, client: CogniteClient, dry_run: bool = False) -> Iterable[UploadResult]:
122
127
  schema, to_export = self._prepare_schema_and_exporters(rules, client)
@@ -266,13 +271,13 @@ class DMSExporter(CDFExporter[DMSSchema]):
266
271
  schema = self.export(rules)
267
272
  to_export: list[tuple[CogniteResourceList, ResourceLoader]] = []
268
273
  if self.export_components.intersection({"all", "spaces"}):
269
- to_export.append((schema.spaces, SpaceLoader(client)))
274
+ to_export.append((SpaceApplyList(schema.spaces.values()), SpaceLoader(client)))
270
275
  if self.export_components.intersection({"all", "containers"}):
271
- to_export.append((schema.containers, ContainerLoader(client)))
276
+ to_export.append((ContainerApplyList(schema.containers.values()), ContainerLoader(client)))
272
277
  if self.export_components.intersection({"all", "views"}):
273
- to_export.append((schema.views, ViewLoader(client, self.existing_handling)))
278
+ to_export.append((ViewApplyList(schema.views.values()), ViewLoader(client, self.existing_handling)))
274
279
  if self.export_components.intersection({"all", "data_models"}):
275
- to_export.append((schema.data_models, DataModelLoader(client)))
280
+ to_export.append((DataModelApplyList([schema.data_model]), DataModelLoader(client)))
276
281
  if isinstance(schema, PipelineSchema):
277
282
  to_export.append((schema.databases, RawDatabaseLoader(client)))
278
283
  to_export.append((schema.raw_tables, RawTableLoader(client)))
@@ -6,7 +6,7 @@ from typing import Any, Literal, cast, overload
6
6
 
7
7
  from cognite.client import CogniteClient
8
8
  from cognite.client import data_modeling as dm
9
- from cognite.client.data_classes.data_modeling import DataModelIdentifier
9
+ from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier
10
10
  from cognite.client.data_classes.data_modeling.containers import BTreeIndex, InvertedIndex
11
11
  from cognite.client.data_classes.data_modeling.views import (
12
12
  MultiEdgeConnectionApply,
@@ -39,7 +39,6 @@ from cognite.neat.rules.models.dms import (
39
39
  from cognite.neat.rules.models.entities import (
40
40
  ClassEntity,
41
41
  ContainerEntity,
42
- DataModelEntity,
43
42
  DMSUnknownEntity,
44
43
  ViewEntity,
45
44
  ViewPropertyEntity,
@@ -52,56 +51,92 @@ class DMSImporter(BaseImporter):
52
51
  schema: DMSSchema,
53
52
  read_issues: Sequence[ValidationIssue] | None = None,
54
53
  metadata: DMSMetadata | None = None,
54
+ ref_metadata: DMSMetadata | None = None,
55
55
  ):
56
56
  # Calling this root schema to distinguish it from
57
57
  # * User Schema
58
58
  # * Reference Schema
59
59
  self.root_schema = schema
60
60
  self.metadata = metadata
61
+ self.ref_metadata = ref_metadata
61
62
  self.issue_list = IssueList(read_issues)
62
- self._all_containers_by_id = {container.as_id(): container for container in schema.containers}
63
+ self._all_containers_by_id = schema.containers.copy()
63
64
  if self.root_schema.reference:
64
- self._all_containers_by_id.update(
65
- {container.as_id(): container for container in self.root_schema.reference.containers}
66
- )
65
+ self._all_containers_by_id.update(self.root_schema.reference.containers)
67
66
 
68
67
  @classmethod
69
- def from_data_model_id(cls, client: CogniteClient, data_model_id: DataModelIdentifier) -> "DMSImporter":
68
+ def from_data_model_id(
69
+ cls,
70
+ client: CogniteClient,
71
+ data_model_id: DataModelIdentifier,
72
+ reference_model_id: DataModelIdentifier | None = None,
73
+ ) -> "DMSImporter":
70
74
  """Create a DMSImporter ready to convert the given data model to rules.
71
75
 
72
76
  Args:
73
77
  client: Instantiated CogniteClient to retrieve data model.
78
+ reference_model_id: The reference data model to retrieve. This is the data model that
79
+ the given data model is built on top of, typically, an enterprise data model.
74
80
  data_model_id: Data Model to retrieve.
75
81
 
76
82
  Returns:
77
83
  DMSImporter: DMSImporter instance
78
84
  """
79
- data_models = client.data_modeling.data_models.retrieve(data_model_id, inline_views=True)
80
- if len(data_models) == 0:
85
+ data_model_ids = [data_model_id, reference_model_id] if reference_model_id else [data_model_id]
86
+ data_models = client.data_modeling.data_models.retrieve(data_model_ids, inline_views=True)
87
+
88
+ user_models = cls._find_model_in_list(data_models, data_model_id)
89
+ if len(user_models) == 0:
81
90
  return cls(DMSSchema(), [issues.importing.NoDataModelError(f"Data model {data_model_id} not found")])
82
- data_model = data_models.latest_version()
91
+ user_model = user_models.latest_version()
92
+
93
+ if reference_model_id:
94
+ ref_models = cls._find_model_in_list(data_models, reference_model_id)
95
+ if len(ref_models) == 0:
96
+ return cls(
97
+ DMSSchema(), [issues.importing.NoDataModelError(f"Data model {reference_model_id} not found")]
98
+ )
99
+ ref_model: dm.DataModel[dm.View] | None = ref_models.latest_version()
100
+ else:
101
+ ref_model = None
83
102
 
84
103
  try:
85
- schema = DMSSchema.from_data_model(client, data_model)
104
+ schema = DMSSchema.from_data_model(client, user_model, ref_model)
86
105
  except Exception as e:
87
106
  return cls(DMSSchema(), [issues.importing.APIError(str(e))])
88
107
 
89
- created = ms_to_datetime(data_model.created_time)
90
- updated = ms_to_datetime(data_model.last_updated_time)
108
+ metadata = cls._create_metadata_from_model(user_model)
109
+ ref_metadata = cls._create_metadata_from_model(ref_model) if ref_model else None
91
110
 
92
- metadata = cls._create_metadata_from_model(data_model, created, updated)
111
+ return cls(schema, [], metadata, ref_metadata)
93
112
 
94
- return cls(schema, [], metadata)
113
+ @classmethod
114
+ def _find_model_in_list(
115
+ cls, data_models: dm.DataModelList[dm.View], model_id: DataModelIdentifier
116
+ ) -> dm.DataModelList[dm.View]:
117
+ identifier = DataModelId.load(model_id)
118
+ return dm.DataModelList[dm.View](
119
+ [
120
+ model
121
+ for model in data_models
122
+ if (model.space, model.external_id) == (identifier.space, identifier.external_id)
123
+ ]
124
+ )
95
125
 
96
126
  @classmethod
97
127
  def _create_metadata_from_model(
98
128
  cls,
99
129
  model: dm.DataModel[dm.View] | dm.DataModelApply,
100
- created: datetime | None = None,
101
- updated: datetime | None = None,
102
130
  ) -> DMSMetadata:
103
131
  description, creator = DMSMetadata._get_description_and_creator(model.description)
104
- now = datetime.now().replace(microsecond=0)
132
+
133
+ if isinstance(model, dm.DataModel):
134
+ created = ms_to_datetime(model.created_time)
135
+ updated = ms_to_datetime(model.last_updated_time)
136
+ else:
137
+ now = datetime.now().replace(microsecond=0)
138
+ created = now
139
+ updated = now
105
140
  return DMSMetadata(
106
141
  schema_=SchemaCompleteness.complete,
107
142
  extension=ExtensionCategory.addition,
@@ -109,8 +144,8 @@ class DMSImporter(BaseImporter):
109
144
  external_id=model.external_id,
110
145
  name=model.name or model.external_id,
111
146
  version=model.version or "0.1.0",
112
- updated=updated or now,
113
- created=created or now,
147
+ updated=updated,
148
+ created=created,
114
149
  creator=creator,
115
150
  description=description,
116
151
  )
@@ -147,28 +182,33 @@ class DMSImporter(BaseImporter):
147
182
  # In case there were errors during the import, the to_rules method will return None
148
183
  return self._return_or_raise(self.issue_list, errors)
149
184
 
150
- if len(self.root_schema.data_models) == 0:
185
+ if not self.root_schema.data_model:
151
186
  self.issue_list.append(issues.importing.NoDataModelError("No data model found."))
152
187
  return self._return_or_raise(self.issue_list, errors)
153
-
188
+ model = self.root_schema.data_model
154
189
  with _handle_issues(
155
190
  self.issue_list,
156
191
  ) as future:
157
192
  schema_completeness = SchemaCompleteness.complete
158
193
  data_model_type = DataModelType.enterprise
159
194
  reference: DMSRules | None = None
160
- if ref_schema := self.root_schema.reference:
195
+ if (ref_schema := self.root_schema.reference) and (ref_model := ref_schema.data_model):
161
196
  # Reference should always be an enterprise model.
162
197
  reference = DMSRules(
163
198
  **self._create_rule_components(
164
- ref_schema, self._create_default_metadata(ref_schema.views), DataModelType.enterprise
199
+ ref_model,
200
+ ref_schema,
201
+ self.ref_metadata or self._create_default_metadata(list(ref_schema.views.values())),
202
+ DataModelType.enterprise,
165
203
  )
166
204
  )
167
205
  schema_completeness = SchemaCompleteness.extended
168
206
  data_model_type = DataModelType.solution
169
207
 
170
208
  user_rules = DMSRules(
171
- **self._create_rule_components(self.root_schema, self.metadata, data_model_type, schema_completeness),
209
+ **self._create_rule_components(
210
+ model, self.root_schema, self.metadata, data_model_type, schema_completeness
211
+ ),
172
212
  reference=reference,
173
213
  )
174
214
 
@@ -179,24 +219,14 @@ class DMSImporter(BaseImporter):
179
219
 
180
220
  def _create_rule_components(
181
221
  self,
222
+ data_model: dm.DataModelApply,
182
223
  schema: DMSSchema,
183
224
  metadata: DMSMetadata | None = None,
184
225
  data_model_type: DataModelType | None = None,
185
226
  schema_completeness: SchemaCompleteness | None = None,
186
227
  ) -> dict[str, Any]:
187
- if len(schema.data_models) > 2:
188
- # Creating a DataModelEntity to convert the data model id to a string.
189
- self.issue_list.append(
190
- issues.importing.MultipleDataModelsWarning(
191
- [str(DataModelEntity.from_id(model.as_id())) for model in schema.data_models]
192
- )
193
- )
194
-
195
- data_model = schema.data_models[0]
196
-
197
228
  properties = SheetList[DMSProperty]()
198
- for view in schema.views:
199
- view_id = view.as_id()
229
+ for view_id, view in schema.views.items():
200
230
  view_entity = ViewEntity.from_id(view_id)
201
231
  class_entity = view_entity.as_class()
202
232
  for prop_id, prop in (view.properties or {}).items():
@@ -217,10 +247,13 @@ class DMSImporter(BaseImporter):
217
247
  metadata=metadata,
218
248
  properties=properties,
219
249
  containers=SheetList[DMSContainer](
220
- data=[DMSContainer.from_container(container) for container in schema.containers]
250
+ data=[DMSContainer.from_container(container) for container in schema.containers.values()]
221
251
  ),
222
252
  views=SheetList[DMSView](
223
- data=[DMSView.from_view(view, in_model=view.as_id() in data_model_view_ids) for view in schema.views]
253
+ data=[
254
+ DMSView.from_view(view, in_model=view_id in data_model_view_ids)
255
+ for view_id, view in schema.views.items()
256
+ ]
224
257
  ),
225
258
  )
226
259
 
@@ -22,6 +22,7 @@ from cognite.neat.rules.models.entities import (
22
22
  ViewPropertyEntity,
23
23
  )
24
24
  from cognite.neat.rules.models.wrapped_entities import DMSFilter, HasDataFilter, NodeTypeFilter
25
+ from cognite.neat.utils.cdf_classes import ContainerApplyDict, NodeApplyDict, SpaceApplyDict, ViewApplyDict
25
26
 
26
27
  from ._rules import DMSMetadata, DMSProperty, DMSRules, DMSView
27
28
  from ._schema import DMSSchema, PipelineSchema
@@ -42,18 +43,18 @@ class _DMSExporter:
42
43
  def __init__(
43
44
  self,
44
45
  rules: DMSRules,
45
- include_ref: bool = True,
46
46
  include_pipeline: bool = False,
47
47
  instance_space: str | None = None,
48
48
  ):
49
- self.include_ref = include_ref
50
49
  self.include_pipeline = include_pipeline
51
50
  self.instance_space = instance_space
52
51
  self.rules = rules
53
52
  self._ref_schema = rules.reference.as_schema() if rules.reference else None
54
53
  if self._ref_schema:
55
54
  # We skip version as that will always be missing in the reference
56
- self._ref_views_by_id = {dm.ViewId(view.space, view.external_id): view for view in self._ref_schema.views}
55
+ self._ref_views_by_id = {
56
+ dm.ViewId(view.space, view.external_id): view for view in self._ref_schema.views.values()
57
+ }
57
58
  else:
58
59
  self._ref_views_by_id = {}
59
60
 
@@ -67,7 +68,7 @@ class _DMSExporter:
67
68
  views_not_in_model = {view.view.as_id() for view in rules.views if not view.in_model}
68
69
  data_model = rules.metadata.as_data_model()
69
70
  data_model.views = sorted(
70
- [view_id for view_id in views.as_ids() if view_id not in views_not_in_model],
71
+ [view_id for view_id in views.keys() if view_id not in views_not_in_model],
71
72
  key=lambda v: v.as_tuple(), # type: ignore[union-attr]
72
73
  )
73
74
 
@@ -75,7 +76,7 @@ class _DMSExporter:
75
76
 
76
77
  output = DMSSchema(
77
78
  spaces=spaces,
78
- data_models=dm.DataModelApplyList([data_model]),
79
+ data_model=data_model,
79
80
  views=views,
80
81
  containers=containers,
81
82
  node_types=node_types,
@@ -91,31 +92,30 @@ class _DMSExporter:
91
92
  def _create_spaces(
92
93
  self,
93
94
  metadata: DMSMetadata,
94
- containers: dm.ContainerApplyList,
95
- views: dm.ViewApplyList,
95
+ containers: ContainerApplyDict,
96
+ views: ViewApplyDict,
96
97
  data_model: dm.DataModelApply,
97
- ) -> dm.SpaceApplyList:
98
- used_spaces = {container.space for container in containers} | {view.space for view in views}
98
+ ) -> SpaceApplyDict:
99
+ used_spaces = {container.space for container in containers.values()} | {view.space for view in views.values()}
99
100
  if len(used_spaces) == 1:
100
101
  # We skip the default space and only use this space for the data model
101
102
  data_model.space = used_spaces.pop()
102
- spaces = dm.SpaceApplyList([dm.SpaceApply(space=data_model.space)])
103
+ spaces = SpaceApplyDict([dm.SpaceApply(space=data_model.space)])
103
104
  else:
104
105
  used_spaces.add(metadata.space)
105
- spaces = dm.SpaceApplyList([dm.SpaceApply(space=space) for space in used_spaces])
106
- if self.instance_space and self.instance_space not in {space.space for space in spaces}:
107
- spaces.append(dm.SpaceApply(space=self.instance_space, name=self.instance_space))
106
+ spaces = SpaceApplyDict([dm.SpaceApply(space=space) for space in used_spaces])
107
+ if self.instance_space and self.instance_space not in spaces:
108
+ spaces[self.instance_space] = dm.SpaceApply(space=self.instance_space, name=self.instance_space)
108
109
  return spaces
109
110
 
110
111
  def _create_views_with_node_types(
111
112
  self,
112
113
  view_properties_by_id: dict[dm.ViewId, list[DMSProperty]],
113
- ) -> tuple[dm.ViewApplyList, dm.NodeApplyList]:
114
- views = dm.ViewApplyList([dms_view.as_view() for dms_view in self.rules.views])
114
+ ) -> tuple[ViewApplyDict, NodeApplyDict]:
115
+ views = ViewApplyDict([dms_view.as_view() for dms_view in self.rules.views])
115
116
  dms_view_by_id = {dms_view.view.as_id(): dms_view for dms_view in self.rules.views}
116
117
 
117
- for view in views:
118
- view_id = view.as_id()
118
+ for view_id, view in views.items():
119
119
  view.properties = {}
120
120
  if not (view_properties := view_properties_by_id.get(view_id)):
121
121
  continue
@@ -126,14 +126,13 @@ class _DMSExporter:
126
126
 
127
127
  data_model_type = self.rules.metadata.data_model_type
128
128
  unique_node_types: set[dm.NodeId] = set()
129
- parent_views = {parent for view in views for parent in view.implements or []}
130
- for view in views:
131
- dms_view = dms_view_by_id.get(view.as_id())
132
- dms_properties = view_properties_by_id.get(view.as_id(), [])
129
+ parent_views = {parent for view in views.values() for parent in view.implements or []}
130
+ for view_id, view in views.items():
131
+ dms_view = dms_view_by_id.get(view_id)
132
+ dms_properties = view_properties_by_id.get(view_id, [])
133
133
  view_filter = self._create_view_filter(view, dms_view, data_model_type, dms_properties)
134
134
 
135
135
  view.filter = view_filter.as_dms_filter()
136
-
137
136
  if isinstance(view_filter, NodeTypeFilter):
138
137
  unique_node_types.update(view_filter.nodes)
139
138
  if view.as_id() in parent_views:
@@ -153,7 +152,7 @@ class _DMSExporter:
153
152
  issues.dms.HasDataFilterOnViewWithReferencesWarning(view.as_id(), list(references)), stacklevel=2
154
153
  )
155
154
 
156
- return views, dm.NodeApplyList(
155
+ return views, NodeApplyDict(
157
156
  [dm.NodeApply(space=node.space, external_id=node.external_id) for node in unique_node_types]
158
157
  )
159
158
 
@@ -176,7 +175,7 @@ class _DMSExporter:
176
175
  def _create_containers(
177
176
  self,
178
177
  container_properties_by_id: dict[dm.ContainerId, list[DMSProperty]],
179
- ) -> dm.ContainerApplyList:
178
+ ) -> ContainerApplyDict:
180
179
  containers = dm.ContainerApplyList(
181
180
  [dms_container.as_container() for dms_container in self.rules.containers or []]
182
181
  )
@@ -231,9 +230,7 @@ class _DMSExporter:
231
230
  for name, const in container.constraints.items()
232
231
  if not (isinstance(const, dm.RequiresConstraint) and const.require in container_to_drop)
233
232
  }
234
- return dm.ContainerApplyList(
235
- [container for container in containers if container.as_id() not in container_to_drop]
236
- )
233
+ return ContainerApplyDict([container for container in containers if container.as_id() not in container_to_drop])
237
234
 
238
235
  def _gather_properties(self) -> tuple[dict[dm.ContainerId, list[DMSProperty]], dict[dm.ViewId, list[DMSProperty]]]:
239
236
  container_properties_by_id: dict[dm.ContainerId, list[DMSProperty]] = defaultdict(list)
@@ -256,6 +253,7 @@ class _DMSExporter:
256
253
  dms_properties: list[DMSProperty],
257
254
  ) -> DMSFilter:
258
255
  selected_filter_name = (dms_view and dms_view.filter_ and dms_view.filter_.name) or ""
256
+
259
257
  if dms_view and dms_view.filter_ and not dms_view.filter_.is_empty:
260
258
  # Has Explicit Filter, use it
261
259
  return dms_view.filter_
@@ -41,7 +41,7 @@ from cognite.neat.rules.models.entities import (
41
41
  ViewEntityList,
42
42
  ViewPropertyEntity,
43
43
  )
44
- from cognite.neat.rules.models.wrapped_entities import HasDataFilter, NodeTypeFilter
44
+ from cognite.neat.rules.models.wrapped_entities import HasDataFilter, NodeTypeFilter, RawFilter
45
45
 
46
46
  from ._schema import DMSSchema
47
47
 
@@ -257,7 +257,7 @@ class DMSView(SheetEntity):
257
257
  description: str | None = Field(alias="Description", default=None)
258
258
  implements: ViewEntityList | None = Field(None, alias="Implements")
259
259
  reference: URLEntity | ReferenceEntity | None = Field(alias="Reference", default=None, union_mode="left_to_right")
260
- filter_: HasDataFilter | NodeTypeFilter | None = Field(None, alias="Filter")
260
+ filter_: HasDataFilter | NodeTypeFilter | RawFilter | None = Field(None, alias="Filter")
261
261
  in_model: bool = Field(True, alias="In Model")
262
262
  class_: ClassEntity = Field(alias="Class (linage)")
263
263
 
@@ -345,12 +345,10 @@ class DMSRules(BaseRules):
345
345
  space, version = self.metadata.space, self.metadata.version
346
346
  return _DMSRulesSerializer(info, space, version).clean(dumped)
347
347
 
348
- def as_schema(
349
- self, include_ref: bool = False, include_pipeline: bool = False, instance_space: str | None = None
350
- ) -> DMSSchema:
348
+ def as_schema(self, include_pipeline: bool = False, instance_space: str | None = None) -> DMSSchema:
351
349
  from ._exporter import _DMSExporter
352
350
 
353
- return _DMSExporter(self, include_ref, include_pipeline, instance_space).to_schema()
351
+ return _DMSExporter(self, include_pipeline, instance_space).to_schema()
354
352
 
355
353
  def as_information_architect_rules(self) -> "InformationRules":
356
354
  from ._converter import _DMSRulesConverter
@@ -30,6 +30,13 @@ from cognite.neat.rules.issues.dms import (
30
30
  MissingViewError,
31
31
  )
32
32
  from cognite.neat.rules.models.data_types import _DATA_TYPE_BY_DMS_TYPE
33
+ from cognite.neat.utils.cdf_classes import (
34
+ CogniteResourceDict,
35
+ ContainerApplyDict,
36
+ NodeApplyDict,
37
+ SpaceApplyDict,
38
+ ViewApplyDict,
39
+ )
33
40
  from cognite.neat.utils.cdf_loaders import ViewLoader
34
41
  from cognite.neat.utils.cdf_loaders.data_classes import RawTableWrite, RawTableWriteList
35
42
  from cognite.neat.utils.text import to_camel
@@ -43,12 +50,12 @@ else:
43
50
 
44
51
  @dataclass
45
52
  class DMSSchema:
46
- data_models: dm.DataModelApplyList = field(default_factory=lambda: dm.DataModelApplyList([]))
47
- spaces: dm.SpaceApplyList = field(default_factory=lambda: dm.SpaceApplyList([]))
48
- views: dm.ViewApplyList = field(default_factory=lambda: dm.ViewApplyList([]))
49
- containers: dm.ContainerApplyList = field(default_factory=lambda: dm.ContainerApplyList([]))
50
- node_types: dm.NodeApplyList = field(default_factory=lambda: dm.NodeApplyList([]))
51
- # The last schema is the previous version of the data model. In the case, extension=additio, this
53
+ data_model: dm.DataModelApply | None = None
54
+ spaces: SpaceApplyDict = field(default_factory=SpaceApplyDict)
55
+ views: ViewApplyDict = field(default_factory=ViewApplyDict)
56
+ containers: ContainerApplyDict = field(default_factory=ContainerApplyDict)
57
+ node_types: NodeApplyDict = field(default_factory=NodeApplyDict)
58
+ # The last schema is the previous version of the data model. In the case, extension=addition, this
52
59
  # should not be modified.
53
60
  last: "DMSSchema | None" = None
54
61
  # Reference is typically the Enterprise model, while this is the solution model.
@@ -57,29 +64,28 @@ class DMSSchema:
57
64
  _FIELD_NAME_BY_RESOURCE_TYPE: ClassVar[dict[str, str]] = {
58
65
  "container": "containers",
59
66
  "view": "views",
60
- "datamodel": "data_models",
67
+ "datamodel": "data_model",
61
68
  "space": "spaces",
62
69
  "node": "node_types",
63
70
  }
64
71
 
65
72
  def _get_mapped_container_from_view(self, view_id: dm.ViewId) -> set[dm.ContainerId]:
66
73
  # index all views, including ones from reference
67
- indexed_views = {
68
- **{view.as_id(): view for view in self.views},
69
- **({view.as_id(): view for view in self.reference.views} if self.reference else {}),
70
- }
74
+ view_by_id = self.views.copy()
75
+ if self.reference:
76
+ view_by_id.update(self.reference.views)
71
77
 
72
- if view_id not in indexed_views:
78
+ if view_id not in view_by_id:
73
79
  raise ValueError(f"View {view_id} not found")
74
80
 
75
- indexed_implemented_views = {id_: view.implements for id_, view in indexed_views.items()}
81
+ indexed_implemented_views = {id_: view.implements for id_, view in view_by_id.items()}
76
82
  view_inheritance = get_inheritance_path(view_id, indexed_implemented_views)
77
83
 
78
- directly_referenced_containers = indexed_views[view_id].referenced_containers()
84
+ directly_referenced_containers = view_by_id[view_id].referenced_containers()
79
85
  inherited_referenced_containers = set()
80
86
 
81
87
  for view_id in view_inheritance:
82
- if implemented_view := indexed_views.get(view_id):
88
+ if implemented_view := view_by_id.get(view_id):
83
89
  inherited_referenced_containers |= implemented_view.referenced_containers()
84
90
  else:
85
91
  raise IncompleteSchemaError(missing_component=view_id).as_exception()
@@ -95,18 +101,49 @@ class DMSSchema:
95
101
  return cls.from_data_model(client, data_model)
96
102
 
97
103
  @classmethod
98
- def from_data_model(cls, client: CogniteClient, data_model: dm.DataModel) -> "DMSSchema":
104
+ def from_data_model(
105
+ cls,
106
+ client: CogniteClient,
107
+ data_model: dm.DataModel[dm.View],
108
+ reference_model: dm.DataModel[dm.View] | None = None,
109
+ ) -> "DMSSchema":
110
+ """Create a schema from a data model.
111
+
112
+ If a reference model is provided, the schema will include a reference schema. To determine which views,
113
+ and containers to put in the reference schema, the following rule is applied:
114
+
115
+ If a view or container space is different from the data model space,
116
+ it will be included in the reference schema.*
117
+
118
+ *One exception to this rule is if a view is directly referenced by the data model. In this case, the view will
119
+ be included in the data model schema, even if the space is different.
120
+
121
+ Args:
122
+ client: The Cognite client used for retrieving components referenced by the data model.
123
+ data_model: The data model to create the schema from.
124
+ reference_model: (Optional) The reference model to include in the schema.
125
+ This is typically the Enterprise model.
126
+
127
+ Returns:
128
+ DMSSchema: The schema created from the data model.
129
+ """
99
130
  views = dm.ViewList(data_model.views)
131
+
132
+ data_model_write = data_model.as_write()
133
+ data_model_write.views = list(views.as_ids())
134
+
135
+ if reference_model:
136
+ views.extend(reference_model.views)
137
+
100
138
  container_ids = views.referenced_containers()
101
139
  containers = client.data_modeling.containers.retrieve(list(container_ids))
102
140
  cls._append_referenced_containers(client, containers)
103
141
 
104
- space_read = client.data_modeling.spaces.retrieve(data_model.space)
105
- if space_read is None:
106
- raise ValueError(f"Space {data_model.space} not found")
107
- space = space_read.as_write()
108
- data_model_write = data_model.as_write()
109
- data_model_write.views = list(views.as_write())
142
+ space_ids = [data_model.space, reference_model.space] if reference_model else [data_model.space]
143
+ space_read = client.data_modeling.spaces.retrieve(space_ids)
144
+ if len(space_read) != len(space_ids):
145
+ raise ValueError(f"Space(s) {space_read} not found")
146
+ space_write = space_read.as_write()
110
147
 
111
148
  view_loader = ViewLoader(client)
112
149
  # We need to include parent views in the schema to make sure that the schema is valid.
@@ -119,13 +156,51 @@ class DMSSchema:
119
156
  # as the read format contains all properties from all parents, while the write formate should not contain
120
157
  # properties from any parents.
121
158
  # The ViewLoader as_write method looks up parents and remove properties from them.
122
- view_write = dm.ViewApplyList([view_loader.as_write(view) for view in views])
159
+ view_write = ViewApplyDict([view_loader.as_write(view) for view in views])
160
+
161
+ container_write = ContainerApplyDict(containers.as_write())
162
+ user_space = data_model.space
163
+ if reference_model:
164
+ user_model_view_ids = set(data_model_write.views)
165
+ ref_model_write = reference_model.as_write()
166
+ ref_model_write.views = [view.as_id() for view in reference_model.views]
167
+
168
+ ref_views = ViewApplyDict(
169
+ [
170
+ view
171
+ for view_id, view in view_write.items()
172
+ if (view.space != user_space) or (view_id not in user_model_view_ids)
173
+ ]
174
+ )
175
+ view_write = ViewApplyDict(
176
+ [
177
+ view
178
+ for view_id, view in view_write.items()
179
+ if view.space == user_space or view_id in user_model_view_ids
180
+ ]
181
+ )
182
+
183
+ ref_containers = ContainerApplyDict(
184
+ [container for container in container_write.values() if container.space != user_space]
185
+ )
186
+ container_write = ContainerApplyDict(
187
+ [container for container in container_write.values() if container.space == user_space]
188
+ )
123
189
 
190
+ ref_schema: DMSSchema | None = cls(
191
+ spaces=SpaceApplyDict([s for s in space_write if s.space != user_space]),
192
+ data_model=ref_model_write,
193
+ views=ref_views,
194
+ containers=ref_containers,
195
+ )
196
+ else:
197
+ ref_schema = None
124
198
  return cls(
125
- spaces=dm.SpaceApplyList([space]),
126
- data_models=dm.DataModelApplyList([data_model_write]),
199
+ spaces=SpaceApplyDict([s for s in space_write if s.space == user_space]),
200
+ data_model=data_model_write,
127
201
  views=view_write,
128
- containers=containers.as_write(),
202
+ containers=container_write,
203
+ reference=ref_schema,
129
204
  )
130
205
 
131
206
  @classmethod
@@ -183,33 +258,32 @@ class DMSSchema:
183
258
  data_models = path_dir / "data_models"
184
259
  data_models.mkdir(parents=True, exist_ok=True)
185
260
  if "spaces" not in exclude_set:
186
- for space in self.spaces:
261
+ for space in self.spaces.values():
187
262
  (data_models / f"{space.space}.space.yaml").write_text(
188
263
  space.dump_yaml(), newline=new_line, encoding=encoding
189
264
  )
190
- if "data_models" not in exclude_set:
191
- for model in self.data_models:
192
- (data_models / f"{model.external_id}.datamodel.yaml").write_text(
193
- model.dump_yaml(), newline=new_line, encoding=encoding
194
- )
265
+ if "data_models" not in exclude_set and self.data_model:
266
+ (data_models / f"{self.data_model.external_id}.datamodel.yaml").write_text(
267
+ self.data_model.dump_yaml(), newline=new_line, encoding=encoding
268
+ )
195
269
  if "views" not in exclude_set and self.views:
196
270
  view_dir = data_models / "views"
197
271
  view_dir.mkdir(parents=True, exist_ok=True)
198
- for view in self.views:
272
+ for view in self.views.values():
199
273
  (view_dir / f"{view.external_id}.view.yaml").write_text(
200
274
  view.dump_yaml(), newline=new_line, encoding=encoding
201
275
  )
202
276
  if "containers" not in exclude_set and self.containers:
203
277
  container_dir = data_models / "containers"
204
278
  container_dir.mkdir(parents=True, exist_ok=True)
205
- for container in self.containers:
279
+ for container in self.containers.values():
206
280
  (container_dir / f"{container.external_id}.container.yaml").write_text(
207
281
  container.dump_yaml(), newline=new_line, encoding=encoding
208
282
  )
209
283
  if "node_types" not in exclude_set and self.node_types:
210
284
  node_dir = data_models / "nodes"
211
285
  node_dir.mkdir(parents=True, exist_ok=True)
212
- for node in self.node_types:
286
+ for node in self.node_types.values():
213
287
  (node_dir / f"{node.external_id}.node.yaml").write_text(
214
288
  node.dump_yaml(), newline=new_line, encoding=encoding
215
289
  )
@@ -263,21 +337,22 @@ class DMSSchema:
263
337
  exclude_set = exclude or set()
264
338
  with zipfile.ZipFile(zip_file, "w") as zip_ref:
265
339
  if "spaces" not in exclude_set:
266
- for space in self.spaces:
340
+ for space in self.spaces.values():
267
341
  zip_ref.writestr(f"data_models/{space.space}.space.yaml", space.dump_yaml())
268
- if "data_models" not in exclude_set:
269
- for model in self.data_models:
270
- zip_ref.writestr(f"data_models/{model.external_id}.datamodel.yaml", model.dump_yaml())
342
+ if "data_models" not in exclude_set and self.data_model:
343
+ zip_ref.writestr(
344
+ f"data_models/{self.data_model.external_id}.datamodel.yaml", self.data_model.dump_yaml()
345
+ )
271
346
  if "views" not in exclude_set:
272
- for view in self.views:
347
+ for view in self.views.values():
273
348
  zip_ref.writestr(f"data_models/views/{view.external_id}.view.yaml", view.dump_yaml())
274
349
  if "containers" not in exclude_set:
275
- for container in self.containers:
350
+ for container in self.containers.values():
276
351
  zip_ref.writestr(
277
352
  f"data_models/containers{container.external_id}.container.yaml", container.dump_yaml()
278
353
  )
279
354
  if "node_types" not in exclude_set:
280
- for node in self.node_types:
355
+ for node in self.node_types.values():
281
356
  zip_ref.writestr(f"data_models/nodes/{node.external_id}.node.yaml", node.dump_yaml())
282
357
 
283
358
  @classmethod
@@ -308,10 +383,30 @@ class DMSSchema:
308
383
  loaded: dict[str, Any] = {}
309
384
  for attr in fields(cls):
310
385
  if items := data_dict.get(attr.name) or data_dict.get(to_camel(attr.name)):
311
- try:
312
- loaded[attr.name] = attr.type.load(items)
313
- except Exception as e:
314
- loaded[attr.name] = cls._load_individual_resources(items, attr, str(e), context.get(attr.name, []))
386
+ if attr.name == "data_model":
387
+ if isinstance(items, list) and len(items) > 1:
388
+ warnings.warn(
389
+ issues.importing.MultipleDataModelsWarning(
390
+ [item.get("externalId", "Unknown") for item in items]
391
+ ),
392
+ stacklevel=2,
393
+ )
394
+ item = items[0] if isinstance(items, list) else items
395
+ try:
396
+ loaded[attr.name] = dm.DataModelApply.load(item)
397
+ except Exception as e:
398
+ data_model_file = context.get(attr.name, [Path("UNKNOWN")])[0]
399
+ warnings.warn(
400
+ issues.fileread.FailedLoadWarning(data_model_file, dm.DataModelApply.__name__, str(e)),
401
+ stacklevel=2,
402
+ )
403
+ else:
404
+ try:
405
+ loaded[attr.name] = attr.type.load(items)
406
+ except Exception as e:
407
+ loaded[attr.name] = cls._load_individual_resources(
408
+ items, attr, str(e), context.get(attr.name, [])
409
+ )
315
410
  return cls(**loaded)
316
411
 
317
412
  @classmethod
@@ -353,9 +448,16 @@ class DMSSchema:
353
448
  cls_fields = sorted(fields(self), key=lambda f: f.name) if sort else fields(self)
354
449
  for attr in cls_fields:
355
450
  if items := getattr(self, attr.name):
356
- items = sorted(items, key=self._to_sortable_identifier) if sort else items
357
451
  key = to_camel(attr.name) if camel_case else attr.name
358
- output[key] = [item.dump(camel_case=camel_case) for item in items]
452
+ if isinstance(items, CogniteResourceDict):
453
+ if sort:
454
+ output[key] = [
455
+ item.dump(camel_case) for item in sorted(items.values(), key=self._to_sortable_identifier)
456
+ ]
457
+ else:
458
+ output[key] = items.dump(camel_case)
459
+ else:
460
+ output[key] = items.dump(camel_case=camel_case)
359
461
  return output
360
462
 
361
463
  @classmethod
@@ -376,19 +478,19 @@ class DMSSchema:
376
478
 
377
479
  def validate(self) -> list[DMSSchemaError]:
378
480
  errors: set[DMSSchemaError] = set()
379
- defined_spaces = {space.space for space in self.spaces}
380
- defined_containers = {container.as_id(): container for container in self.containers}
381
- defined_views = {view.as_id() for view in self.views}
481
+ defined_spaces = self.spaces.copy()
482
+ defined_containers = self.containers.copy()
483
+ defined_views = self.views.copy()
382
484
  if self.reference:
383
- defined_spaces |= {space.space for space in self.reference.spaces}
384
- defined_containers |= {container.as_id(): container for container in self.reference.containers}
385
- defined_views |= {view.as_id() for view in self.reference.views}
485
+ defined_spaces |= self.reference.spaces
486
+ defined_containers |= self.reference.containers
487
+ defined_views |= self.reference.views
386
488
 
387
- for container in self.containers:
489
+ for container in self.containers.values():
388
490
  if container.space not in defined_spaces:
389
491
  errors.add(MissingSpaceError(space=container.space, referred_by=container.as_id()))
390
492
 
391
- for view in self.views:
493
+ for view in self.views.values():
392
494
  view_id = view.as_id()
393
495
  if view.space not in defined_spaces:
394
496
  errors.add(MissingSpaceError(space=view.space, referred_by=view_id))
@@ -458,7 +560,8 @@ class DMSSchema:
458
560
  )
459
561
  )
460
562
 
461
- for model in self.data_models:
563
+ if self.data_model:
564
+ model = self.data_model
462
565
  if model.space not in defined_spaces:
463
566
  errors.add(MissingSpaceError(space=model.space, referred_by=model.as_id()))
464
567
 
@@ -506,16 +609,27 @@ class DMSSchema:
506
609
  )
507
610
  return None
508
611
 
509
- def referenced_spaces(self) -> set[str]:
510
- referenced_spaces = {container.space for container in self.containers}
511
- referenced_spaces |= {view.space for view in self.views}
512
- referenced_spaces |= {container.space for view in self.views for container in view.referenced_containers()}
513
- referenced_spaces |= {parent.space for view in self.views for parent in view.implements or []}
514
- referenced_spaces |= {node.space for node in self.node_types}
515
- referenced_spaces |= {model.space for model in self.data_models}
516
- referenced_spaces |= {view.space for model in self.data_models for view in model.views or []}
517
- referenced_spaces |= {s.space for s in self.spaces}
612
+ def referenced_spaces(self, include_indirect_references: bool = True) -> set[str]:
613
+ """Get the spaces referenced by the schema.
518
614
 
615
+ Args:
616
+ include_indirect_references (bool): If True, the spaces referenced by as view.implements, and
617
+ view.referenced_containers will be included in the output.
618
+ Returns:
619
+ set[str]: The spaces referenced by the schema.
620
+ """
621
+ referenced_spaces = {view.space for view in self.views.values()}
622
+ referenced_spaces |= {container.space for container in self.containers.values()}
623
+ if include_indirect_references:
624
+ referenced_spaces |= {
625
+ container.space for view in self.views.values() for container in view.referenced_containers()
626
+ }
627
+ referenced_spaces |= {parent.space for view in self.views.values() for parent in view.implements or []}
628
+ referenced_spaces |= {node.space for node in self.node_types.values()}
629
+ if self.data_model:
630
+ referenced_spaces |= {self.data_model.space}
631
+ referenced_spaces |= {view.space for view in self.data_model.views or []}
632
+ referenced_spaces |= {s.space for s in self.spaces.values()}
519
633
  return referenced_spaces
520
634
 
521
635
 
@@ -625,19 +739,19 @@ class PipelineSchema(DMSSchema):
625
739
 
626
740
  @classmethod
627
741
  def from_dms(cls, schema: DMSSchema, instance_space: str | None = None) -> "PipelineSchema":
628
- if not schema.data_models:
742
+ if not schema.data_model:
629
743
  raise ValueError("PipelineSchema must contain at least one data model")
630
- first_data_model = schema.data_models[0]
744
+ first_data_model = schema.data_model
631
745
  # The database name is limited to 32 characters
632
746
  database_name = first_data_model.external_id[:32]
633
747
  instance_space = instance_space or first_data_model.space
634
748
  database = DatabaseWrite(name=database_name)
635
- parent_views = {parent for view in schema.views for parent in view.implements or []}
636
- container_by_id = {container.as_id(): container for container in schema.containers}
749
+ parent_views = {parent for view in schema.views.values() for parent in view.implements or []}
750
+ container_by_id = schema.containers.copy()
637
751
 
638
752
  transformations = TransformationWriteList([])
639
753
  raw_tables = RawTableWriteList([])
640
- for view in schema.views:
754
+ for view in schema.views.values():
641
755
  if view.as_id() in parent_views:
642
756
  # Skipping parents as they do not have their own data
643
757
  continue
@@ -666,7 +780,7 @@ class PipelineSchema(DMSSchema):
666
780
 
667
781
  return cls(
668
782
  spaces=schema.spaces,
669
- data_models=schema.data_models,
783
+ data_model=schema.data_model,
670
784
  views=schema.views,
671
785
  containers=schema.containers,
672
786
  transformations=transformations,
@@ -164,10 +164,10 @@ class DMSPostValidation:
164
164
  # Everything is allowed
165
165
  return None
166
166
  # Is an extension of an existing model.
167
- user_schema = self.rules.as_schema(include_ref=False)
167
+ user_schema = self.rules.as_schema()
168
168
  ref_schema = self.rules.reference.as_schema()
169
- new_containers = {container.as_id(): container for container in user_schema.containers}
170
- existing_containers = {container.as_id(): container for container in ref_schema.containers}
169
+ new_containers = user_schema.containers.copy()
170
+ existing_containers = ref_schema.containers.copy()
171
171
 
172
172
  for container_id, container in new_containers.items():
173
173
  existing_container = existing_containers.get(container_id)
@@ -193,8 +193,8 @@ class DMSPostValidation:
193
193
  # Reshape allows changes to views
194
194
  return None
195
195
 
196
- new_views = {view.as_id(): view for view in user_schema.views}
197
- existing_views = {view.as_id(): view for view in ref_schema.views}
196
+ new_views = user_schema.views.copy()
197
+ existing_views = ref_schema.views.copy()
198
198
  for view_id, view in new_views.items():
199
199
  existing_view = existing_views.get(view_id)
200
200
  if not existing_view or existing_view == view:
@@ -219,13 +219,13 @@ class DMSPostValidation:
219
219
 
220
220
  dms_schema = self.rules.as_schema()
221
221
 
222
- for view in dms_schema.views:
223
- mapped_containers = dms_schema._get_mapped_container_from_view(view.as_id())
222
+ for view_id, view in dms_schema.views.items():
223
+ mapped_containers = dms_schema._get_mapped_container_from_view(view_id)
224
224
 
225
225
  if mapped_containers and len(mapped_containers) > 10:
226
226
  self.issue_list.append(
227
227
  issues.dms.ViewMapsToTooManyContainersWarning(
228
- view_id=view.as_id(),
228
+ view_id=view_id,
229
229
  container_ids=mapped_containers,
230
230
  )
231
231
  )
@@ -236,7 +236,7 @@ class DMSPostValidation:
236
236
  ):
237
237
  self.issue_list.append(
238
238
  issues.dms.HasDataFilterAppliedToTooManyContainersWarning(
239
- view_id=view.as_id(),
239
+ view_id=view_id,
240
240
  container_ids=mapped_containers,
241
241
  )
242
242
  )
@@ -1,3 +1,5 @@
1
+ import json
2
+ import re
1
3
  from abc import ABC, abstractmethod
2
4
  from collections.abc import Collection
3
5
  from functools import total_ordering
@@ -37,8 +39,19 @@ class WrappedEntity(BaseModel, ABC):
37
39
  def _parse(cls, data: str) -> dict:
38
40
  if data.casefold() == cls.name.casefold():
39
41
  return {"inner": None}
40
- inner = data[len(cls.name) :].removeprefix("(").removesuffix(")")
41
- return {"inner": [cls._inner_cls.load(entry.strip()) for entry in inner.split(",")]}
42
+
43
+ # raw filter case:
44
+ if cls.__name__ == "RawFilter":
45
+ if match := re.search(r"rawFilter\(([\s\S]*?)\)", data):
46
+ return {"filter": match.group(1), "inner": None}
47
+ else:
48
+ raise ValueError(f"Cannot parse {cls.name} from {data}. Ill formatted raw filter.")
49
+
50
+ # nodeType and hasData case:
51
+ elif inner := data[len(cls.name) :].removeprefix("(").removesuffix(")"):
52
+ return {"inner": [cls._inner_cls.load(entry.strip()) for entry in inner.split(",")]}
53
+ else:
54
+ raise ValueError(f"Cannot parse {cls.name} from {data}")
42
55
 
43
56
  @model_serializer(when_used="unless-none", return_type=str)
44
57
  def as_str(self) -> str:
@@ -164,3 +177,22 @@ class HasDataFilter(DMSFilter):
164
177
  # Sorting to ensure deterministic order
165
178
  containers=sorted(containers, key=lambda container: container.as_tuple()) # type: ignore[union-attr]
166
179
  )
180
+
181
+
182
+ class RawFilter(DMSFilter):
183
+ name: ClassVar[str] = "rawFilter"
184
+ filter: str
185
+ inner: None = None # type: ignore[assignment]
186
+
187
+ def as_dms_filter(self) -> dm.Filter: # type: ignore[override]
188
+ try:
189
+ return dm.Filter.load(json.loads(self.filter))
190
+ except json.JSONDecodeError as e:
191
+ raise ValueError(f"Error loading raw filter: {e}") from e
192
+
193
+ @property
194
+ def is_empty(self) -> bool:
195
+ return self.filter is None
196
+
197
+ def __repr__(self) -> str:
198
+ return self.filter
@@ -0,0 +1,181 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import (
3
+ ItemsView,
4
+ Iterable,
5
+ Iterator,
6
+ KeysView,
7
+ Mapping,
8
+ MutableMapping,
9
+ ValuesView,
10
+ )
11
+ from typing import Any, TypeVar, cast, final
12
+
13
+ import pandas as pd
14
+ import yaml
15
+ from cognite.client.data_classes._base import T_CogniteResource
16
+ from cognite.client.data_classes.data_modeling import (
17
+ ContainerApply,
18
+ ContainerId,
19
+ DataModelApply,
20
+ DataModelId,
21
+ NodeApply,
22
+ NodeId,
23
+ SpaceApply,
24
+ ViewApply,
25
+ ViewId,
26
+ )
27
+ from cognite.client.utils._auxiliary import load_yaml_or_json
28
+ from cognite.client.utils._pandas_helpers import (
29
+ convert_nullable_int_cols,
30
+ )
31
+
32
+ T_ID = TypeVar("T_ID")
33
+
34
+
35
+ # Inheriting from dict as we are extending it,
36
+ # ref https://stackoverflow.com/questions/7148419/subclass-dict-userdict-dict-or-abc
37
+ class CogniteResourceDict(dict, MutableMapping[T_ID, T_CogniteResource], ABC):
38
+ """CogniteResource stored in a mapping structure.
39
+
40
+ The serialization format of the CognitiveResourceDict is a list of dicts, where each dict
41
+ represents a CognitiveResource.
42
+
43
+ This means that the serialization methods .dump() and .load() return a list of dicts and
44
+ expects a list of dicts respectively.
45
+
46
+ In addition, the init method is slightly abused compared to a regular dict by allowing the input to be a
47
+ list of CognitiveResources.
48
+ """
49
+
50
+ _RESOURCE: type[T_CogniteResource]
51
+
52
+ def __init__(
53
+ self,
54
+ items: Iterable[T_CogniteResource]
55
+ | Iterable[tuple[T_ID, T_CogniteResource]]
56
+ | Mapping[T_ID, T_CogniteResource]
57
+ | None = None,
58
+ ) -> None:
59
+ if isinstance(items, Mapping):
60
+ super().__init__(items)
61
+ elif isinstance(items, Iterable):
62
+ super().__init__(item if isinstance(item, tuple) else (self._as_id(item), item) for item in items) # type: ignore[arg-type]
63
+ else:
64
+ super().__init__()
65
+
66
+ @classmethod
67
+ @abstractmethod
68
+ def _as_id(cls, resource: T_CogniteResource) -> T_ID:
69
+ raise NotImplementedError
70
+
71
+ def dump(self, camel_case: bool = True) -> list[dict[str, Any]]:
72
+ return [value.dump(camel_case) for value in self.values()]
73
+
74
+ def dump_yaml(self) -> str:
75
+ return yaml.dump(self.dump(camel_case=True), sort_keys=False)
76
+
77
+ def to_pandas(self, camel_case: bool = False) -> pd.DataFrame:
78
+ df = pd.DataFrame(self.dump(camel_case=camel_case))
79
+ df = convert_nullable_int_cols(df)
80
+ return df
81
+
82
+ def _repr_html_(self) -> str:
83
+ # Pretty print the dataframe in Jupyter
84
+ return self.to_pandas()._repr_html_() # type: ignore[operator]
85
+
86
+ @classmethod
87
+ @final
88
+ def load(cls: "type[T_CogniteResourceDict]", resource: Iterable[dict[str, Any]] | str) -> "T_CogniteResourceDict":
89
+ """Load a resource from a YAML/JSON string or iterable of dict."""
90
+ if isinstance(resource, str):
91
+ resource = load_yaml_or_json(resource)
92
+
93
+ if isinstance(resource, Iterable):
94
+ return cls._load(cast(Iterable, resource))
95
+
96
+ raise TypeError(f"Resource must be json or yaml str, or iterable of dicts, not {type(resource)}")
97
+
98
+ @classmethod
99
+ def _load(
100
+ cls: "type[T_CogniteResourceDict]",
101
+ resource_list: Iterable[dict[str, Any]],
102
+ ) -> "T_CogniteResourceDict":
103
+ resources = (cls._RESOURCE._load(resource) for resource in resource_list)
104
+ return cls({cls._as_id(resource): resource for resource in resources}) # type: ignore[abstract]
105
+
106
+ # The below methods are included to make better type hints in the IDE
107
+ def __getitem__(self, k: T_ID) -> T_CogniteResource:
108
+ return super().__getitem__(k)
109
+
110
+ def __setitem__(self, k: T_ID, v: T_CogniteResource) -> None:
111
+ super().__setitem__(k, v)
112
+
113
+ def __delitem__(self, k: T_ID) -> None:
114
+ super().__delitem__(k)
115
+
116
+ def __iter__(self) -> Iterator[T_ID]:
117
+ return super().__iter__()
118
+
119
+ def keys(self) -> KeysView[T_ID]: # type: ignore[override]
120
+ return super().keys()
121
+
122
+ def values(self) -> ValuesView[T_CogniteResource]: # type: ignore[override]
123
+ return super().values()
124
+
125
+ def items(self) -> ItemsView[T_ID, T_CogniteResource]: # type: ignore[override]
126
+ return super().items()
127
+
128
+ def get(self, __key: T_ID, __default: Any = ...) -> T_CogniteResource:
129
+ return super().get(__key, __default)
130
+
131
+ def pop(self, __key: T_ID, __default: Any = ...) -> T_CogniteResource:
132
+ return super().pop(__key, __default)
133
+
134
+ def popitem(self) -> tuple[T_ID, T_CogniteResource]:
135
+ return super().popitem()
136
+
137
+ def copy(self) -> "CogniteResourceDict[T_ID, T_CogniteResource]":
138
+ return cast(CogniteResourceDict[T_ID, T_CogniteResource], super().copy())
139
+
140
+
141
+ T_CogniteResourceDict = TypeVar("T_CogniteResourceDict", bound=CogniteResourceDict)
142
+
143
+
144
+ class ViewApplyDict(CogniteResourceDict[ViewId, ViewApply]):
145
+ _RESOURCE = ViewApply
146
+
147
+ @classmethod
148
+ def _as_id(cls, resource: ViewApply) -> ViewId:
149
+ return resource.as_id()
150
+
151
+
152
+ class SpaceApplyDict(CogniteResourceDict[str, SpaceApply]):
153
+ _RESOURCE = SpaceApply
154
+
155
+ @classmethod
156
+ def _as_id(cls, resource: SpaceApply) -> str:
157
+ return resource.space
158
+
159
+
160
+ class ContainerApplyDict(CogniteResourceDict[ContainerId, ContainerApply]):
161
+ _RESOURCE = ContainerApply
162
+
163
+ @classmethod
164
+ def _as_id(cls, resource: ContainerApply) -> ContainerId:
165
+ return resource.as_id()
166
+
167
+
168
+ class DataModelApplyDict(CogniteResourceDict[DataModelId, DataModelApply]):
169
+ _RESOURCE = DataModelApply
170
+
171
+ @classmethod
172
+ def _as_id(cls, resource: DataModelApply) -> DataModelId:
173
+ return resource.as_id()
174
+
175
+
176
+ class NodeApplyDict(CogniteResourceDict[NodeId, NodeApply]):
177
+ _RESOURCE = NodeApply
178
+
179
+ @classmethod
180
+ def _as_id(cls, resource: NodeApply) -> NodeId:
181
+ return resource.as_id()
@@ -78,9 +78,9 @@ class ValidateRulesAgainstCDF(Step):
78
78
  f"and {len(retrieved_views)} views from CDF."
79
79
  )
80
80
 
81
- schema.spaces.extend(retrieved_spaces)
82
- schema.containers.extend(retrieved_containers)
83
- schema.views.extend(retrieved_views)
81
+ schema.spaces.update({space.space: space for space in retrieved_spaces})
82
+ schema.containers.update({container.as_id(): container for container in retrieved_containers})
83
+ schema.views.update({view.as_id(): view for view in retrieved_views})
84
84
 
85
85
  errors = schema.validate()
86
86
  if errors:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cognite-neat
3
- Version: 0.76.3
3
+ Version: 0.77.1
4
4
  Summary: Knowledge graph transformation
5
5
  Home-page: https://cognite-neat.readthedocs-hosted.com/
6
6
  License: Apache-2.0
@@ -1,5 +1,5 @@
1
1
  cognite/neat/__init__.py,sha256=v-rRiDOgZ3sQSMQKq0vgUQZvpeOkoHFXissAx6Ktg84,61
2
- cognite/neat/_version.py,sha256=OtWfvEnAHBJj0F1t4KUE2UmEyH4tIaTqHJzrcWyW6tI,23
2
+ cognite/neat/_version.py,sha256=KAGTP0ql_FPeQE1WIVlTa-PVmeudJNxVGVpT_IPdAEg,23
3
3
  cognite/neat/app/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  cognite/neat/app/api/asgi/metrics.py,sha256=nxFy7L5cChTI0a-zkCiJ59Aq8yLuIJp5c9Dg0wRXtV0,152
5
5
  cognite/neat/app/api/configuration.py,sha256=2U5M6M252swvQPQyooA1EBzFUZNtcTmuSaywfJDgckM,4232
@@ -165,14 +165,14 @@ cognite/neat/rules/exceptions.py,sha256=YLnsbXXJdDSr_szQoioEtOdqDV8PR7RdQjpMP2SW
165
165
  cognite/neat/rules/exporters/__init__.py,sha256=Gn3CjkVKHJF9Po1ZPH4wAJ-sRW9up7b2CpXm-eReV3Q,413
166
166
  cognite/neat/rules/exporters/_base.py,sha256=m63iw8xjlZbZAxGL8mn7pjGf1pW3rVv8C20_RSiu4t0,1511
167
167
  cognite/neat/rules/exporters/_models.py,sha256=vRd0P_YsrZ1eaAGGHfdTeFunaqHdaa0ZtnWiVZBR1nc,1976
168
- cognite/neat/rules/exporters/_rules2dms.py,sha256=BNznUtTdJ__M10I7QQf3_zdIQTET8SGFvHv5a-5louM,13529
168
+ cognite/neat/rules/exporters/_rules2dms.py,sha256=US2IO4YTJSF_CDzau1dTpXyeHntJWVSPkMCQoUoKeC0,13688
169
169
  cognite/neat/rules/exporters/_rules2excel.py,sha256=K3D_AC6UZ-cG9ZFkqFvuDiMTdBC9ZUW9_IkkY9KsYW0,14934
170
170
  cognite/neat/rules/exporters/_rules2ontology.py,sha256=NWS3cn2927LQqW_PdQ-92OLIlmIKGNk7xh5yOMAyj94,20120
171
171
  cognite/neat/rules/exporters/_rules2yaml.py,sha256=sOSdnTJ5mXuyAJECdNnNsX6oLvgETptkpgPUQbK0n2w,3026
172
172
  cognite/neat/rules/exporters/_validation.py,sha256=OlKIyf4nhSDehJwFHDQ8Zdf6HpNfW7dSe2s67eywHu4,4078
173
173
  cognite/neat/rules/importers/__init__.py,sha256=zqNbGpvdVhYkLjWx1i9dJ3FXzYGtuQyTydUYsj-BndQ,408
174
174
  cognite/neat/rules/importers/_base.py,sha256=GUiJrYwJ25thI71iS9hCeP_iSZ0Vv8ou3z6MfD07FAk,4274
175
- cognite/neat/rules/importers/_dms2rules.py,sha256=HNUDix1xwNILpyb_wxwrOqnhSvdMiPKYfk4JL7AT6ks,16891
175
+ cognite/neat/rules/importers/_dms2rules.py,sha256=UxAatq8WL0rYz4wcopY0wHcBE4JTf--FzflhMxymypY,18274
176
176
  cognite/neat/rules/importers/_dtdl2rules/__init__.py,sha256=CNR-sUihs2mnR1bPMKs3j3L4ds3vFTsrl6YycExZTfU,68
177
177
  cognite/neat/rules/importers/_dtdl2rules/_unit_lookup.py,sha256=wW4saKva61Q_i17guY0dc4OseJDQfqHy_QZBtm0OD6g,12134
178
178
  cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py,sha256=ysmWUxZ0npwrTB0uiH5jA0v37sfCwowGaYk17IyxPUU,12663
@@ -202,21 +202,22 @@ cognite/neat/rules/models/_types/_field.py,sha256=74WfCSVbTubpK4n4VsysQqCch6VI8I
202
202
  cognite/neat/rules/models/data_types.py,sha256=lanwkhwG8iHKfjYfia4v2SBTJrMeXOsqaVkVEP2QMXs,6078
203
203
  cognite/neat/rules/models/dms/__init__.py,sha256=Wzyqzz2ZIjpUbDg04CMuuIAw-f2A02DayNeqO9R-2Hw,491
204
204
  cognite/neat/rules/models/dms/_converter.py,sha256=x3u3jLnkknozoXXoAXXOWFHCsppqUwSvWv9wMOJ2F1Y,5706
205
- cognite/neat/rules/models/dms/_exporter.py,sha256=9EVToiib2QSxEXOzP7-xSlitpUlWSZTHar1HLIrtqxI,18908
206
- cognite/neat/rules/models/dms/_rules.py,sha256=CesPmLT4rC7rB_Q895nIV34lwfg3RC9NoU1wsz5l-hA,15599
205
+ cognite/neat/rules/models/dms/_exporter.py,sha256=WTokr7gyjrpxW212kZpFfvd7zw09RRVPjAsxe_aizFo,18903
206
+ cognite/neat/rules/models/dms/_rules.py,sha256=ZLdJLdAJGX1l5t1hChEic9TOHdculbWRhiLbUluZRRQ,15568
207
207
  cognite/neat/rules/models/dms/_rules_input.py,sha256=qfInh3JYf7XGEghxPOtIj7GY0f5_aVvnYeUBmfGV9mk,13620
208
- cognite/neat/rules/models/dms/_schema.py,sha256=cUAkIp7TCYVCshLZwQJcJMj_BduGSI3Y4_LwVxHUUUM,37463
208
+ cognite/neat/rules/models/dms/_schema.py,sha256=6lJSLPoZAjFnbfFl6dmAJR0xlAsQKi_Obcxp8Lp_t-0,41973
209
209
  cognite/neat/rules/models/dms/_serializer.py,sha256=Zulj__rnaVNtrbGJPkn4dYMfMXWYyRmtNPR2Yb5zYW0,6668
210
- cognite/neat/rules/models/dms/_validation.py,sha256=QaNBqKi9YGqkYw_cwO_FcH3vRI2dXAORC6fjqV3D9HM,14290
210
+ cognite/neat/rules/models/dms/_validation.py,sha256=rctt6ImSK5-fHhReH-BXbMlYu_EkTRYYE5c_JS-EQgM,14141
211
211
  cognite/neat/rules/models/domain.py,sha256=13OhG-XavE5ipU2ICaYaUhz60volkuVfbJrsp0PhaUU,2993
212
212
  cognite/neat/rules/models/entities.py,sha256=iBG84Jr1qQ7PvkMJUJzJ1oWApeONb1IACixdJSztUhk,16395
213
213
  cognite/neat/rules/models/information/__init__.py,sha256=KvbYxVk38qReGbGTrU_Y3P3Gz6Bfghk5lHSKs8DlTOI,195
214
214
  cognite/neat/rules/models/information/_converter.py,sha256=jzaIk7Q2CeU3TIGULEINwUNNyhWu-VdOW646EjH_FrI,7964
215
215
  cognite/neat/rules/models/information/_rules.py,sha256=YE7X8MsPQv-AVtl4vYtQW99moT45sYk2dI2DDS1YRO0,15546
216
- cognite/neat/rules/models/wrapped_entities.py,sha256=c5GkzOrYrE6SSRzIS2r8OAjhwxXpOoAO1WGc8kwiPPo,6154
216
+ cognite/neat/rules/models/wrapped_entities.py,sha256=ThhjnNNrpgz0HeORIQ8Q894trxP73P7T_TuZj6qH2CU,7157
217
217
  cognite/neat/utils/__init__.py,sha256=l5Nyqhqo25bcQXCOb_lk01cr-UXsG8cczz_y_I0u6bg,68
218
218
  cognite/neat/utils/auxiliary.py,sha256=E2-YtddzScvN7l7j0kNYIMlfqIUT9NWMqLpcJYPK4rY,309
219
219
  cognite/neat/utils/cdf.py,sha256=dTg8wnm2916yhWT_2Jg9_PlauHCbmnuNgmpqrGU8eO0,711
220
+ cognite/neat/utils/cdf_classes.py,sha256=NEmz5UprBlqfqZnqJkRk5xjSpzazwHbhcWsMH_GNxP8,5831
220
221
  cognite/neat/utils/cdf_loaders/__init__.py,sha256=s2aPR5XLo6WZ0ybstAJlcGFYkA7CyHW1XO-NYpL0V6o,483
221
222
  cognite/neat/utils/cdf_loaders/_base.py,sha256=j85HINIx4poGD-0dN3JPHTqjOS1XhCxv7G9s1gC1GBo,2057
222
223
  cognite/neat/utils/cdf_loaders/_data_modeling.py,sha256=RoIj2cL_j3vP6e4BlCExZU8d96p3dLitU9nsIIyesLc,11336
@@ -255,7 +256,7 @@ cognite/neat/workflows/steps/lib/current/graph_loader.py,sha256=HfGg1HRZhbV58TFu
255
256
  cognite/neat/workflows/steps/lib/current/graph_store.py,sha256=r7VTxdaz8jJQU7FJbnRDMxvEYbSAZFNMABhPyfNwiFk,6295
256
257
  cognite/neat/workflows/steps/lib/current/rules_exporter.py,sha256=wUQAZXWBCqWXe0241QSREtnNTii_tSmOkeiSPwNQRjk,22898
257
258
  cognite/neat/workflows/steps/lib/current/rules_importer.py,sha256=yDq06cvxLvEpSnTXTjwhxDie_MzHa3wO1A4cbKnrH6c,10338
258
- cognite/neat/workflows/steps/lib/current/rules_validator.py,sha256=fDRQiRHN9Cuph38-WruK0T1UG5H448S_GsbzdOpi0h4,4729
259
+ cognite/neat/workflows/steps/lib/current/rules_validator.py,sha256=LwF9lXlnuPOxDSsOMMTHBi2BHc5rk08IF5zahS9yQuw,4844
259
260
  cognite/neat/workflows/steps/lib/io/__init__.py,sha256=k7IPbIq3ey19oRc5sA_15F99-O6dxzqbm1LihGRRo5A,32
260
261
  cognite/neat/workflows/steps/lib/io/io_steps.py,sha256=QAGypoi1vP32BRiIgBZ0B4qsbFMcwhzpRiVUUnWysLA,16874
261
262
  cognite/neat/workflows/steps/lib/legacy/__init__.py,sha256=725aFzVqhE0tbVOAW70zWXTGKFiYImVupRZ4C5_IkUo,274
@@ -271,8 +272,8 @@ cognite/neat/workflows/steps_registry.py,sha256=fkTX14ZA7_gkUYfWIlx7A1XbCidvqR23
271
272
  cognite/neat/workflows/tasks.py,sha256=dqlJwKAb0jlkl7abbY8RRz3m7MT4SK8-7cntMWkOYjw,788
272
273
  cognite/neat/workflows/triggers.py,sha256=_BLNplzoz0iic367u1mhHMHiUrCwP-SLK6_CZzfODX0,7071
273
274
  cognite/neat/workflows/utils.py,sha256=gKdy3RLG7ctRhbCRwaDIWpL9Mi98zm56-d4jfHDqP1E,453
274
- cognite_neat-0.76.3.dist-info/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
275
- cognite_neat-0.76.3.dist-info/METADATA,sha256=N-fbQdrmuZ820x4fcdayOruzEbkDoCD1TU3scIJhWxo,9316
276
- cognite_neat-0.76.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
277
- cognite_neat-0.76.3.dist-info/entry_points.txt,sha256=61FPqiWb25vbqB0KI7znG8nsg_ibLHBvTjYnkPvNFso,50
278
- cognite_neat-0.76.3.dist-info/RECORD,,
275
+ cognite_neat-0.77.1.dist-info/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
276
+ cognite_neat-0.77.1.dist-info/METADATA,sha256=CXlHgF0btEegP5yLRqUsR40q8fIh3zaMU9nAefHikpQ,9316
277
+ cognite_neat-0.77.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
278
+ cognite_neat-0.77.1.dist-info/entry_points.txt,sha256=61FPqiWb25vbqB0KI7znG8nsg_ibLHBvTjYnkPvNFso,50
279
+ cognite_neat-0.77.1.dist-info/RECORD,,