cognite-neat 0.99.1__py3-none-any.whl → 0.100.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cognite-neat might be problematic. Click here for more details.

Files changed (47) hide show
  1. cognite/neat/_client/_api/data_modeling_loaders.py +403 -182
  2. cognite/neat/_client/data_classes/data_modeling.py +4 -0
  3. cognite/neat/_graph/extractors/_base.py +7 -0
  4. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +23 -13
  5. cognite/neat/_graph/loaders/_rdf2dms.py +50 -11
  6. cognite/neat/_graph/transformers/__init__.py +3 -3
  7. cognite/neat/_graph/transformers/_classic_cdf.py +120 -52
  8. cognite/neat/_issues/warnings/__init__.py +2 -0
  9. cognite/neat/_issues/warnings/_resources.py +15 -0
  10. cognite/neat/_rules/analysis/_base.py +15 -5
  11. cognite/neat/_rules/analysis/_dms.py +20 -0
  12. cognite/neat/_rules/analysis/_information.py +22 -0
  13. cognite/neat/_rules/exporters/_base.py +3 -5
  14. cognite/neat/_rules/exporters/_rules2dms.py +192 -200
  15. cognite/neat/_rules/importers/_rdf/_inference2rules.py +22 -5
  16. cognite/neat/_rules/models/_base_rules.py +19 -0
  17. cognite/neat/_rules/models/_types.py +5 -0
  18. cognite/neat/_rules/models/dms/_exporter.py +215 -93
  19. cognite/neat/_rules/models/dms/_rules.py +4 -4
  20. cognite/neat/_rules/models/dms/_rules_input.py +8 -3
  21. cognite/neat/_rules/models/dms/_validation.py +42 -11
  22. cognite/neat/_rules/models/entities/_multi_value.py +3 -0
  23. cognite/neat/_rules/models/information/_rules.py +17 -2
  24. cognite/neat/_rules/models/information/_rules_input.py +11 -2
  25. cognite/neat/_rules/models/information/_validation.py +99 -3
  26. cognite/neat/_rules/models/mapping/_classic2core.yaml +1 -1
  27. cognite/neat/_rules/transformers/__init__.py +2 -1
  28. cognite/neat/_rules/transformers/_converters.py +163 -61
  29. cognite/neat/_rules/transformers/_mapping.py +132 -2
  30. cognite/neat/_session/_base.py +42 -31
  31. cognite/neat/_session/_mapping.py +105 -5
  32. cognite/neat/_session/_prepare.py +43 -9
  33. cognite/neat/_session/_read.py +50 -4
  34. cognite/neat/_session/_set.py +1 -0
  35. cognite/neat/_session/_to.py +36 -13
  36. cognite/neat/_session/_wizard.py +5 -0
  37. cognite/neat/_session/engine/_interface.py +3 -2
  38. cognite/neat/_store/_base.py +79 -19
  39. cognite/neat/_utils/collection_.py +22 -0
  40. cognite/neat/_utils/rdf_.py +24 -0
  41. cognite/neat/_version.py +2 -2
  42. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +3 -3
  43. {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.1.dist-info}/METADATA +1 -1
  44. {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.1.dist-info}/RECORD +47 -47
  45. {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.1.dist-info}/LICENSE +0 -0
  46. {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.1.dist-info}/WHEEL +0 -0
  47. {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.1.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  import warnings
2
2
  from abc import ABC, abstractmethod
3
- from collections.abc import Sequence
3
+ from collections.abc import Callable, Collection, Iterable, Sequence
4
+ from dataclasses import dataclass, field
4
5
  from graphlib import TopologicalSorter
5
- from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, cast
6
+ from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeVar, cast
6
7
 
7
8
  from cognite.client.data_classes import filters
8
9
  from cognite.client.data_classes._base import (
9
- CogniteResourceList,
10
10
  T_CogniteResourceList,
11
11
  T_WritableCogniteResource,
12
12
  T_WriteClass,
@@ -23,6 +23,10 @@ from cognite.client.data_classes.data_modeling import (
23
23
  DataModelList,
24
24
  EdgeConnection,
25
25
  MappedProperty,
26
+ Node,
27
+ NodeApply,
28
+ NodeApplyList,
29
+ NodeList,
26
30
  RequiresConstraint,
27
31
  Space,
28
32
  SpaceApply,
@@ -48,6 +52,8 @@ from cognite.client.data_classes.data_modeling.views import (
48
52
  from cognite.client.exceptions import CogniteAPIError
49
53
  from cognite.client.utils.useful_types import SequenceNotStr
50
54
 
55
+ from cognite.neat._client.data_classes.data_modeling import Component
56
+ from cognite.neat._client.data_classes.schema import DMSSchema
51
57
  from cognite.neat._issues.warnings import CDFMaxIterationsWarning
52
58
  from cognite.neat._shared import T_ID
53
59
 
@@ -56,6 +62,16 @@ if TYPE_CHECKING:
56
62
 
57
63
  T_WritableCogniteResourceList = TypeVar("T_WritableCogniteResourceList", bound=WriteableCogniteResourceList)
58
64
 
65
+ T_Item = TypeVar("T_Item")
66
+ T_Out = TypeVar("T_Out", bound=Iterable)
67
+
68
+
69
+ @dataclass
70
+ class MultiCogniteAPIError(Exception, Generic[T_ID, T_WritableCogniteResourceList]):
71
+ success: T_WritableCogniteResourceList
72
+ failed: list[T_ID] = field(default_factory=list)
73
+ errors: list[CogniteAPIError] = field(default_factory=list)
74
+
59
75
 
60
76
  class ResourceLoader(
61
77
  ABC,
@@ -68,6 +84,7 @@ class ResourceLoader(
68
84
  """
69
85
 
70
86
  resource_name: str
87
+ dependencies: "ClassVar[frozenset[type[ResourceLoader]]]" = frozenset()
71
88
 
72
89
  def __init__(self, client: "NeatClient") -> None:
73
90
  # This is exposed to allow for disabling the cache.
@@ -89,36 +106,79 @@ class ResourceLoader(
89
106
  return [cls.get_id(item) for item in items]
90
107
 
91
108
  def create(self, items: Sequence[T_WriteClass]) -> T_WritableCogniteResourceList:
92
- created = self._create(items)
109
+ # Containers can have dependencies on other containers, so we sort them before creating them.
110
+ items = self.sort_by_dependencies(items)
111
+
112
+ exception: MultiCogniteAPIError[T_ID, T_WritableCogniteResourceList] | None = None
113
+ try:
114
+ created = self._fallback_one_by_one(self._create, items)
115
+ except MultiCogniteAPIError as e:
116
+ created = e.success
117
+ exception = e
118
+
93
119
  if self.cache:
94
120
  self._items_by_id.update({self.get_id(item): item for item in created})
121
+
122
+ if exception is not None:
123
+ raise exception
124
+
95
125
  return created
96
126
 
97
127
  def retrieve(self, ids: SequenceNotStr[T_ID]) -> T_WritableCogniteResourceList:
98
128
  if not self.cache:
99
- return self._retrieve(ids)
129
+ # We now that SequenceNotStr = Sequence
130
+ return self._fallback_one_by_one(self._retrieve, ids) # type: ignore[arg-type]
131
+ exception: MultiCogniteAPIError[T_ID, T_WritableCogniteResourceList] | None = None
100
132
  missing_ids = [id for id in ids if id not in self._items_by_id.keys()]
101
133
  if missing_ids:
102
- retrieved = self._retrieve(missing_ids)
134
+ try:
135
+ retrieved = self._retrieve(missing_ids)
136
+ except MultiCogniteAPIError as e:
137
+ retrieved = e.success
138
+ exception = e
103
139
  self._items_by_id.update({self.get_id(item): item for item in retrieved})
140
+ if exception is not None:
141
+ raise exception
104
142
  # We need to check the cache again, in case we didn't retrieve all the items.
105
143
  return self._create_list([self._items_by_id[id] for id in ids if id in self._items_by_id])
106
144
 
107
- def update(self, items: Sequence[T_WriteClass]) -> T_WritableCogniteResourceList:
108
- if not self.cache:
109
- return self._update(items)
110
- updated = self._update(items)
111
- self._items_by_id.update({self.get_id(item): item for item in updated})
145
+ def update(
146
+ self, items: Sequence[T_WriteClass], force: bool = False, drop_data: bool = False
147
+ ) -> T_WritableCogniteResourceList:
148
+ exception: MultiCogniteAPIError[T_ID, T_WritableCogniteResourceList] | None = None
149
+ if force:
150
+ updated = self._update_force(items, drop_data=drop_data)
151
+ else:
152
+ try:
153
+ updated = self._fallback_one_by_one(self._update, items)
154
+ except MultiCogniteAPIError as e:
155
+ updated = e.success
156
+ exception = e
157
+
158
+ if self.cache:
159
+ self._items_by_id.update({self.get_id(item): item for item in updated})
160
+
161
+ if exception is not None:
162
+ raise exception
163
+
112
164
  return updated
113
165
 
114
166
  def delete(self, ids: SequenceNotStr[T_ID] | Sequence[T_WriteClass]) -> list[T_ID]:
115
167
  id_list = [self.get_id(item) for item in ids]
116
- if not self.cache:
117
- return self._delete(id_list)
118
- ids = [self.get_id(item) for item in ids]
119
- deleted = self._delete(id_list)
120
- for id in deleted:
121
- self._items_by_id.pop(id, None)
168
+ exception: MultiCogniteAPIError[T_ID, T_WritableCogniteResourceList] | None = None
169
+ try:
170
+ # We know that SequenceNotStr = Sequence
171
+ deleted = self._fallback_one_by_one(self._delete, id_list) # type: ignore[arg-type]
172
+ except MultiCogniteAPIError as e:
173
+ deleted = e.success
174
+ exception = e
175
+
176
+ if self.cache:
177
+ for id in deleted:
178
+ self._items_by_id.pop(id, None)
179
+ if exception is not None:
180
+ raise exception
181
+
122
182
  return deleted
123
183
 
124
184
  @abstractmethod
@@ -141,9 +201,77 @@ class ResourceLoader(
141
201
  def _create_list(self, items: Sequence[T_WritableCogniteResource]) -> T_WritableCogniteResourceList:
142
202
  raise NotImplementedError
143
203
 
204
+ def has_data(self, item_id: T_ID) -> bool:
205
+ return False
206
+
144
207
  def are_equal(self, local: T_WriteClass, remote: T_WritableCogniteResource) -> bool:
145
208
  return local == remote.as_write()
146
209
 
210
+ def sort_by_dependencies(self, items: Sequence[T_WriteClass]) -> list[T_WriteClass]:
211
+ return list(items)
212
+
213
+ def _update_force(
214
+ self,
215
+ items: Sequence[T_WriteClass],
216
+ drop_data: bool = False,
217
+ tried_force_update: set[T_ID] | None = None,
218
+ success: T_WritableCogniteResourceList | None = None,
219
+ ) -> T_WritableCogniteResourceList:
220
+ tried_force_update = tried_force_update or set()
221
+ try:
222
+ return self._update(items)
223
+ except CogniteAPIError as e:
224
+ failed_ids = {self.get_id(failed) for failed in e.failed + e.unknown}
225
+ success_ids = [self.get_id(success) for success in e.successful]
226
+ success_ = self.retrieve(success_ids)
227
+ if success is None:
228
+ success = success_
229
+ else:
230
+ success.extend(success_)
231
+ to_redeploy: list[T_WriteClass] = []
232
+ for item in items:
233
+ item_id = self.get_id(item)
234
+ if item_id in failed_ids:
235
+ if tried_force_update and item_id in tried_force_update:
236
+ # Avoid infinite loop
237
+ continue
238
+ tried_force_update.add(item_id)
239
+ if self.has_data(item_id) and not drop_data:
240
+ continue
241
+ to_redeploy.append(item)
242
+ if not to_redeploy:
243
+ # Avoid infinite loop
244
+ raise e
245
+ self.delete(to_redeploy)
246
+ forced = self._update_force(to_redeploy, drop_data, tried_force_update, success)
247
+ forced.extend(success)
248
+ return forced
249
+
250
+ def _fallback_one_by_one(self, method: Callable[[Sequence[T_Item]], T_Out], items: Sequence[T_Item]) -> T_Out:
251
+ try:
252
+ return method(items)
253
+ except CogniteAPIError as e:
254
+ exception = MultiCogniteAPIError[T_ID, T_WritableCogniteResourceList](self._create_list([]))
255
+ success = {self.get_id(success) for success in e.successful}
256
+ if success:
257
+ # Need read version of the items to put into cache.
258
+ retrieve_items = self.retrieve(list(success))
259
+ exception.success.extend(retrieve_items)
260
+ for item in items:
261
+ # We know that item is either T_ID or T_WriteClass
262
+ # but the T_Item cannot be bound to both types at the same time.
263
+ item_id = self.get_id(item) # type: ignore[arg-type]
264
+ if item_id in success:
265
+ continue
266
+ try:
267
+ item_result = method([item])
268
+ except CogniteAPIError as item_exception:
269
+ exception.errors.append(item_exception)
270
+ exception.failed.extend(self.get_ids(item_exception.failed))
271
+ else:
272
+ exception.success.extend(item_result)
273
+ raise exception from None
274
+
147
275
 
148
276
  class DataModelingLoader(
149
277
  ResourceLoader[T_ID, T_WriteClass, T_WritableCogniteResource, T_CogniteResourceList, T_WritableCogniteResourceList],
@@ -155,40 +283,10 @@ class DataModelingLoader(
155
283
  return item.space in space
156
284
  raise ValueError(f"Item {item} does not have a space attribute")
157
285
 
158
- def sort_by_dependencies(self, items: list[T_WriteClass]) -> list[T_WriteClass]:
159
- return items
160
-
161
- def create(
162
- self, items: Sequence[T_WriteClass], existing_handling: Literal["fail", "skip", "update", "force"] = "fail"
163
- ) -> T_WritableCogniteResourceList:
164
- if existing_handling != "force":
165
- return super().create(items)
166
-
167
- created = self._create_force(items, set())
168
- if self.cache:
169
- self._items_by_id.update({self.get_id(item): item for item in created})
170
- return created
171
-
172
- def _create_force(
173
- self,
174
- items: Sequence[T_WriteClass],
175
- tried_force_deploy: set[T_ID],
176
- ) -> T_WritableCogniteResourceList:
177
- try:
178
- return self._create(items)
179
- except CogniteAPIError as e:
180
- failed_ids = {self.get_id(failed) for failed in e.failed}
181
- to_redeploy = [
182
- item
183
- for item in items
184
- if self.get_id(item) in failed_ids and self.get_id(item) not in tried_force_deploy
185
- ]
186
- if not to_redeploy:
187
- # Avoid infinite loop
188
- raise e
189
- tried_force_deploy.update([self.get_id(item) for item in to_redeploy])
190
- self.delete(to_redeploy)
191
- return self._create_force(to_redeploy, tried_force_deploy)
286
+ @classmethod
287
+ @abstractmethod
288
+ def items_from_schema(cls, schema: DMSSchema) -> T_CogniteResourceList:
289
+ raise NotImplementedError
192
290
 
193
291
 
194
292
  class SpaceLoader(DataModelingLoader[str, SpaceApply, Space, SpaceApplyList, SpaceList]):
@@ -260,9 +358,167 @@ class SpaceLoader(DataModelingLoader[str, SpaceApply, Space, SpaceApplyList, Spa
260
358
  deleted_space = self._client.data_modeling.spaces.delete(space)
261
359
  print(f"Deleted space {deleted_space}")
262
360
 
361
+ @classmethod
362
+ def items_from_schema(cls, schema: DMSSchema) -> SpaceApplyList:
363
+ return SpaceApplyList(schema.spaces.values())
364
+
365
+ def has_data(self, item_id: str) -> bool:
366
+ if self._client.data_modeling.instances.list("node", limit=1, space=item_id):
367
+ return True
368
+ if self._client.data_modeling.instances.list("edge", limit=1, space=item_id):
369
+ return True
370
+ # Need to check if there are any containers with data in the space. Typically,
371
+ # a schema space will not contain data, while it will have containers that have data in an instance space.
372
+ for container in self._client.data_modeling.containers(space=item_id, include_global=False):
373
+ if self._client.loaders.containers.has_data(container.as_id()):
374
+ return True
375
+ return False
376
+
377
+
378
+ class ContainerLoader(DataModelingLoader[ContainerId, ContainerApply, Container, ContainerApplyList, ContainerList]):
379
+ resource_name = "containers"
380
+ dependencies = frozenset({SpaceLoader})
381
+
382
+ @classmethod
383
+ def get_id(cls, item: Container | ContainerApply | ContainerId | dict) -> ContainerId:
384
+ if isinstance(item, Container | ContainerApply):
385
+ return item.as_id()
386
+ if isinstance(item, dict):
387
+ return ContainerId.load(item)
388
+ return item
389
+
390
+ def sort_by_dependencies(self, items: Sequence[ContainerApply]) -> list[ContainerApply]:
391
+ container_by_id = {container.as_id(): container for container in items}
392
+ container_dependencies = {
393
+ container.as_id(): {
394
+ const.require
395
+ for const in container.constraints.values()
396
+ if isinstance(const, RequiresConstraint) and const.require in container_by_id
397
+ }
398
+ for container in items
399
+ }
400
+ return [
401
+ container_by_id[container_id] for container_id in TopologicalSorter(container_dependencies).static_order()
402
+ ]
403
+
404
+ def _create(self, items: Sequence[ContainerApply]) -> ContainerList:
405
+ return self._client.data_modeling.containers.apply(items)
406
+
407
+ def retrieve(self, ids: SequenceNotStr[ContainerId], include_connected: bool = False) -> ContainerList:
408
+ if not include_connected:
409
+ return super().retrieve(ids)
410
+ # Retrieve recursively updates the cache.
411
+ return self._retrieve_recursive(ids)
412
+
413
+ def _retrieve(self, ids: SequenceNotStr[ContainerId]) -> ContainerList:
414
+ return self._client.data_modeling.containers.retrieve(cast(Sequence, ids))
415
+
416
+ def _update(self, items: Sequence[ContainerApply]) -> ContainerList:
417
+ return self._create(items)
418
+
419
+ def _delete(self, ids: SequenceNotStr[ContainerId]) -> list[ContainerId]:
420
+ return self._client.data_modeling.containers.delete(cast(Sequence, ids))
421
+
422
+ def _create_list(self, items: Sequence[Container]) -> ContainerList:
423
+ return ContainerList(items)
424
+
425
+ def _retrieve_recursive(self, container_ids: SequenceNotStr[ContainerId]) -> ContainerList:
426
+ """Containers can reference each other through the 'requires' constraint.
427
+
428
+ This method retrieves all containers that are referenced by other containers through the 'requires' constraint,
429
+ including their parents.
430
+ """
431
+ max_iterations = 10 # Limiting the number of iterations to avoid infinite loops
432
+ found = ContainerList([])
433
+ found_ids: set[ContainerId] = set()
434
+ last_batch = list(container_ids)
435
+ for _ in range(max_iterations):
436
+ if not last_batch:
437
+ break
438
+ to_retrieve_from_cdf: set[ContainerId] = set()
439
+ batch_ids: list[ContainerId] = []
440
+ for container_id in last_batch:
441
+ if container_id in found_ids:
442
+ continue
443
+ elif container_id in self._items_by_id:
444
+ container = self._items_by_id[container_id]
445
+ found.append(container)
446
+ batch_ids.extend(self.get_connected_containers(container, found_ids))
447
+ else:
448
+ to_retrieve_from_cdf.add(container_id)
449
+
450
+ if to_retrieve_from_cdf:
451
+ retrieved_batch = self._client.data_modeling.containers.retrieve(list(to_retrieve_from_cdf))
452
+ self._items_by_id.update({view.as_id(): view for view in retrieved_batch})
453
+ found.extend(retrieved_batch)
454
+ found_ids.update({view.as_id() for view in retrieved_batch})
455
+ for container in retrieved_batch:
456
+ batch_ids.extend(self.get_connected_containers(container, found_ids))
457
+
458
+ last_batch = batch_ids
459
+ else:
460
+ warnings.warn(
461
+ CDFMaxIterationsWarning(
462
+ "The maximum number of iterations was reached while resolving referenced containers."
463
+ "There might be referenced containers that are not included in the list of containers.",
464
+ max_iterations=max_iterations,
465
+ ),
466
+ stacklevel=2,
467
+ )
468
+
469
+ if self.cache is False:
470
+ # We must update the cache to retrieve recursively.
471
+ # If the cache is disabled, bust the cache to avoid storing the retrieved views.
472
+ self.bust_cache()
473
+ return found
474
+
475
+ @staticmethod
476
+ def get_connected_containers(
477
+ container: Container | ContainerApply, skip: set[ContainerId] | None = None
478
+ ) -> set[ContainerId]:
479
+ connected_containers = set()
480
+ for constraint in container.constraints.values():
481
+ if isinstance(constraint, RequiresConstraint):
482
+ connected_containers.add(constraint.require)
483
+ if skip:
484
+ return {container_id for container_id in connected_containers if container_id not in skip}
485
+ return connected_containers
486
+
487
+ def are_equal(self, local: ContainerApply, remote: Container) -> bool:
488
+ local_dumped = local.dump(camel_case=True)
489
+ if "usedFor" not in local_dumped:
490
+ # Setting used_for to "node" as it is the default value in the CDF.
491
+ local_dumped["usedFor"] = "node"
492
+
493
+ return local_dumped == remote.as_write().dump(camel_case=True)
494
+
495
+ @classmethod
496
+ def items_from_schema(cls, schema: DMSSchema) -> ContainerApplyList:
497
+ return ContainerApplyList(schema.containers.values())
498
+
499
+ def has_data(self, item_id: ContainerId) -> bool:
500
+ has_data_filter = filters.HasData(containers=[item_id])
501
+ has_data = False
502
+ instance_type: Literal["node", "edge"]
503
+ # Mypy does not understand that the instance type is Literal["node", "edge"]
504
+ for instance_type in ["node", "edge"]: # type: ignore[assignment]
505
+ try:
506
+ has_data = bool(
507
+ self._client.data_modeling.instances.list(instance_type, limit=1, filter=has_data_filter)
508
+ )
509
+ except CogniteAPIError as e:
510
+ if e.code != 400:
511
+ # If the container is used for nodes and we ask for edges, we get a 400 error. This
512
+ # means there is no edge data for this container.
513
+ raise
514
+ if has_data:
515
+ return True
516
+ return has_data
517
+
263
518
 
264
519
  class ViewLoader(DataModelingLoader[ViewId, ViewApply, View, ViewApplyList, ViewList]):
265
520
  resource_name = "views"
521
+ dependencies = frozenset({SpaceLoader, ContainerLoader})
266
522
 
267
523
  @classmethod
268
524
  def get_id(cls, item: View | ViewApply | ViewId | dict) -> ViewId:
@@ -341,19 +597,19 @@ class ViewLoader(DataModelingLoader[ViewId, ViewApply, View, ViewApplyList, View
341
597
  include_connections: Whether to include all connected views.
342
598
  include_ancestors: Whether to include all ancestors.
343
599
  """
344
- last_batch = list(view_ids)
600
+ last_batch = set(view_ids)
345
601
  found = ViewList([])
346
602
  found_ids: set[ViewId] = set()
347
603
  while last_batch:
348
604
  to_retrieve_from_cdf: set[ViewId] = set()
349
- batch_ids: list[ViewId] = []
605
+ batch_ids: set[ViewId] = set()
350
606
  for view_id in last_batch:
351
607
  if view_id in found_ids:
352
608
  continue
353
609
  elif view_id in self._items_by_id:
354
610
  view = self._items_by_id[view_id]
355
611
  found.append(view)
356
- batch_ids.extend(self.get_connected_views(view, include_ancestors, include_connections, found_ids))
612
+ batch_ids.update(self.get_connected_views(view, include_ancestors, include_connections, found_ids))
357
613
  else:
358
614
  to_retrieve_from_cdf.add(view_id)
359
615
 
@@ -363,7 +619,7 @@ class ViewLoader(DataModelingLoader[ViewId, ViewApply, View, ViewApplyList, View
363
619
  found.extend(retrieved_batch)
364
620
  found_ids.update({view.as_id() for view in retrieved_batch})
365
621
  for view in retrieved_batch:
366
- batch_ids.extend(self.get_connected_views(view, include_ancestors, include_connections, found_ids))
622
+ batch_ids.update(self.get_connected_views(view, include_ancestors, include_connections, found_ids))
367
623
 
368
624
  last_batch = batch_ids
369
625
 
@@ -403,126 +659,14 @@ class ViewLoader(DataModelingLoader[ViewId, ViewApply, View, ViewApplyList, View
403
659
  def _create_list(self, items: Sequence[View]) -> ViewList:
404
660
  return ViewList(items)
405
661
 
406
-
407
- class ContainerLoader(DataModelingLoader[ContainerId, ContainerApply, Container, ContainerApplyList, ContainerList]):
408
- resource_name = "containers"
409
-
410
662
  @classmethod
411
- def get_id(cls, item: Container | ContainerApply | ContainerId | dict) -> ContainerId:
412
- if isinstance(item, Container | ContainerApply):
413
- return item.as_id()
414
- if isinstance(item, dict):
415
- return ContainerId.load(item)
416
- return item
417
-
418
- def sort_by_dependencies(self, items: Sequence[ContainerApply]) -> list[ContainerApply]:
419
- container_by_id = {container.as_id(): container for container in items}
420
- container_dependencies = {
421
- container.as_id(): {
422
- const.require
423
- for const in container.constraints.values()
424
- if isinstance(const, RequiresConstraint) and const.require in container_by_id
425
- }
426
- for container in items
427
- }
428
- return [
429
- container_by_id[container_id] for container_id in TopologicalSorter(container_dependencies).static_order()
430
- ]
431
-
432
- def _create(self, items: Sequence[ContainerApply]) -> ContainerList:
433
- return self._client.data_modeling.containers.apply(items)
434
-
435
- def retrieve(self, ids: SequenceNotStr[ContainerId], include_connected: bool = False) -> ContainerList:
436
- if not include_connected:
437
- return super().retrieve(ids)
438
- # Retrieve recursively updates the cache.
439
- return self._retrieve_recursive(ids)
440
-
441
- def _retrieve(self, ids: SequenceNotStr[ContainerId]) -> ContainerList:
442
- return self._client.data_modeling.containers.retrieve(cast(Sequence, ids))
443
-
444
- def _update(self, items: Sequence[ContainerApply]) -> ContainerList:
445
- return self._create(items)
446
-
447
- def _delete(self, ids: SequenceNotStr[ContainerId]) -> list[ContainerId]:
448
- return self._client.data_modeling.containers.delete(cast(Sequence, ids))
449
-
450
- def _create_list(self, items: Sequence[Container]) -> ContainerList:
451
- return ContainerList(items)
452
-
453
- def _retrieve_recursive(self, container_ids: SequenceNotStr[ContainerId]) -> ContainerList:
454
- """Containers can reference each other through the 'requires' constraint.
455
-
456
- This method retrieves all containers that are referenced by other containers through the 'requires' constraint,
457
- including their parents.
458
- """
459
- max_iterations = 10 # Limiting the number of iterations to avoid infinite loops
460
- found = ContainerList([])
461
- found_ids: set[ContainerId] = set()
462
- last_batch = list(container_ids)
463
- for _ in range(max_iterations):
464
- if not last_batch:
465
- break
466
- to_retrieve_from_cdf: set[ContainerId] = set()
467
- batch_ids: list[ContainerId] = []
468
- for container_id in last_batch:
469
- if container_id in found_ids:
470
- continue
471
- elif container_id in self._items_by_id:
472
- container = self._items_by_id[container_id]
473
- found.append(container)
474
- batch_ids.extend(self.get_connected_containers(container, found_ids))
475
- else:
476
- to_retrieve_from_cdf.add(container_id)
477
-
478
- if to_retrieve_from_cdf:
479
- retrieved_batch = self._client.data_modeling.containers.retrieve(list(to_retrieve_from_cdf))
480
- self._items_by_id.update({view.as_id(): view for view in retrieved_batch})
481
- found.extend(retrieved_batch)
482
- found_ids.update({view.as_id() for view in retrieved_batch})
483
- for container in retrieved_batch:
484
- batch_ids.extend(self.get_connected_containers(container, found_ids))
485
-
486
- last_batch = batch_ids
487
- else:
488
- warnings.warn(
489
- CDFMaxIterationsWarning(
490
- "The maximum number of iterations was reached while resolving referenced containers."
491
- "There might be referenced containers that are not included in the list of containers.",
492
- max_iterations=max_iterations,
493
- ),
494
- stacklevel=2,
495
- )
496
-
497
- if self.cache is False:
498
- # We must update the cache to retrieve recursively.
499
- # If the cache is disabled, bust the cache to avoid storing the retrieved views.
500
- self.bust_cache()
501
- return found
502
-
503
- @staticmethod
504
- def get_connected_containers(
505
- container: Container | ContainerApply, skip: set[ContainerId] | None = None
506
- ) -> set[ContainerId]:
507
- connected_containers = set()
508
- for constraint in container.constraints.values():
509
- if isinstance(constraint, RequiresConstraint):
510
- connected_containers.add(constraint.require)
511
- if skip:
512
- return {container_id for container_id in connected_containers if container_id not in skip}
513
- return connected_containers
514
-
515
- def are_equal(self, local: ContainerApply, remote: Container) -> bool:
516
- local_dumped = local.dump(camel_case=True)
517
- if "usedFor" not in local_dumped:
518
- # Setting used_for to "node" as it is the default value in the CDF.
519
- local_dumped["usedFor"] = "node"
520
-
521
- return local_dumped == remote.as_write().dump(camel_case=True)
663
+ def items_from_schema(cls, schema: DMSSchema) -> ViewApplyList:
664
+ return ViewApplyList(schema.views.values())
522
665
 
523
666
 
524
667
  class DataModelLoader(DataModelingLoader[DataModelId, DataModelApply, DataModel, DataModelApplyList, DataModelList]):
525
668
  resource_name = "data_models"
669
+ dependencies = frozenset({SpaceLoader, ViewLoader})
526
670
 
527
671
  @classmethod
528
672
  def get_id(cls, item: DataModel | DataModelApply | DataModelId | dict) -> DataModelId:
@@ -562,6 +706,72 @@ class DataModelLoader(DataModelingLoader[DataModelId, DataModelApply, DataModel,
562
706
 
563
707
  return local_dumped == cdf_resource_dumped
564
708
 
709
+ @classmethod
710
+ def items_from_schema(cls, schema: DMSSchema) -> DataModelApplyList:
711
+ return DataModelApplyList([schema.data_model])
712
+
713
+
714
+ class NodeLoader(DataModelingLoader[NodeId, NodeApply, Node, NodeApplyList, NodeList]):
715
+ resource_name = "nodes"
716
+ dependencies = frozenset({SpaceLoader, ContainerLoader, ViewLoader})
717
+
718
+ @classmethod
719
+ def get_id(cls, item: Node | NodeApply | NodeId | dict) -> NodeId:
720
+ if isinstance(item, Node | NodeApply):
721
+ return item.as_id()
722
+ if isinstance(item, dict):
723
+ return NodeId.load(item)
724
+ return item
725
+
726
+ def _create(self, items: Sequence[NodeApply]) -> NodeList:
727
+ self._client.data_modeling.instances.apply(items)
728
+ return self._retrieve([item.as_id() for item in items])
729
+
730
+ def _retrieve(self, ids: SequenceNotStr[NodeId]) -> NodeList:
731
+ return self._client.data_modeling.instances.retrieve(cast(Sequence, ids)).nodes
732
+
733
+ def _update(self, items: Sequence[NodeApply]) -> NodeList:
734
+ self._client.data_modeling.instances.apply(items, replace=True)
735
+ return self._retrieve([item.as_id() for item in items])
736
+
737
+ def _delete(self, ids: SequenceNotStr[NodeId]) -> list[NodeId]:
738
+ return list(self._client.data_modeling.instances.delete(nodes=cast(Sequence, ids)).nodes)
739
+
740
+ def _create_list(self, items: Sequence[Node]) -> NodeList:
741
+ return NodeList(items)
742
+
743
+ def are_equal(self, local: NodeApply, remote: Node) -> bool:
744
+ local_dumped = local.dump()
745
+
746
+ # Note reading from a container is not supported.
747
+ sources = [
748
+ source_prop_pair.source
749
+ for source_prop_pair in local.sources or []
750
+ if isinstance(source_prop_pair.source, ViewId)
751
+ ]
752
+ if sources:
753
+ try:
754
+ cdf_resource_with_properties = self._client.data_modeling.instances.retrieve(
755
+ nodes=remote.as_id(), sources=sources
756
+ ).nodes[0]
757
+ except CogniteAPIError:
758
+ # View does not exist, so node does not exist.
759
+ return False
760
+ else:
761
+ cdf_resource_with_properties = remote
762
+ cdf_resource_dumped = cdf_resource_with_properties.as_write().dump()
763
+
764
+ if "existingVersion" not in local_dumped:
765
+ # Existing version is typically not set when creating nodes, but we get it back
766
+ # when we retrieve the node from the server.
767
+ local_dumped["existingVersion"] = cdf_resource_dumped.get("existingVersion", None)
768
+
769
+ return local_dumped == cdf_resource_dumped
770
+
771
+ @classmethod
772
+ def items_from_schema(cls, schema: DMSSchema) -> NodeApplyList:
773
+ return NodeApplyList(schema.node_types.values())
774
+
565
775
 
566
776
  class DataModelLoaderAPI:
567
777
  def __init__(self, client: "NeatClient") -> None:
@@ -570,16 +780,27 @@ class DataModelLoaderAPI:
570
780
  self.views = ViewLoader(client)
571
781
  self.containers = ContainerLoader(client)
572
782
  self.data_models = DataModelLoader(client)
783
+ self.nodes = NodeLoader(client)
784
+ self._loaders: list[DataModelingLoader] = [
785
+ self.spaces,
786
+ self.views,
787
+ self.containers,
788
+ self.data_models,
789
+ self.nodes,
790
+ ]
573
791
 
574
- def get_loader(self, items: Any) -> DataModelingLoader:
575
- if isinstance(items, CogniteResourceList):
576
- resource_name = type(items).__name__.casefold().removesuffix("list").removesuffix("apply")
577
- elif isinstance(items, str):
578
- resource_name = items
579
- else:
580
- raise ValueError(f"Cannot determine resource name from {items}")
581
- if resource_name[-1] != "s":
582
- resource_name += "s"
583
- if resource_name == "datamodels":
584
- resource_name = "data_models"
585
- return getattr(self, resource_name)
792
+ def by_dependency_order(
793
+ self, component: Component | Collection[Component] | None = None
794
+ ) -> list[DataModelingLoader]:
795
+ loader_by_type = {type(loader): loader for loader in self._loaders}
796
+ loader_iterable = (
797
+ loader_by_type[loader_cls] # type: ignore[index]
798
+ for loader_cls in TopologicalSorter(
799
+ {type(loader): loader.dependencies for loader in self._loaders} # type: ignore[attr-defined]
800
+ ).static_order()
801
+ )
802
+ if component is None:
803
+ return list(loader_iterable)
804
+ components = {component} if isinstance(component, str) else set(component)
805
+ components = {{"node_type": "nodes"}.get(component, component) for component in components}
806
+ return [loader for loader in loader_iterable if loader.resource_name in components]