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.
- cognite/neat/_client/_api/data_modeling_loaders.py +403 -182
- cognite/neat/_client/data_classes/data_modeling.py +4 -0
- cognite/neat/_graph/extractors/_base.py +7 -0
- cognite/neat/_graph/extractors/_classic_cdf/_classic.py +23 -13
- cognite/neat/_graph/loaders/_rdf2dms.py +50 -11
- cognite/neat/_graph/transformers/__init__.py +3 -3
- cognite/neat/_graph/transformers/_classic_cdf.py +120 -52
- cognite/neat/_issues/warnings/__init__.py +2 -0
- cognite/neat/_issues/warnings/_resources.py +15 -0
- cognite/neat/_rules/analysis/_base.py +15 -5
- cognite/neat/_rules/analysis/_dms.py +20 -0
- cognite/neat/_rules/analysis/_information.py +22 -0
- cognite/neat/_rules/exporters/_base.py +3 -5
- cognite/neat/_rules/exporters/_rules2dms.py +192 -200
- cognite/neat/_rules/importers/_rdf/_inference2rules.py +22 -5
- cognite/neat/_rules/models/_base_rules.py +19 -0
- cognite/neat/_rules/models/_types.py +5 -0
- cognite/neat/_rules/models/dms/_exporter.py +215 -93
- cognite/neat/_rules/models/dms/_rules.py +4 -4
- cognite/neat/_rules/models/dms/_rules_input.py +8 -3
- cognite/neat/_rules/models/dms/_validation.py +42 -11
- cognite/neat/_rules/models/entities/_multi_value.py +3 -0
- cognite/neat/_rules/models/information/_rules.py +17 -2
- cognite/neat/_rules/models/information/_rules_input.py +11 -2
- cognite/neat/_rules/models/information/_validation.py +99 -3
- cognite/neat/_rules/models/mapping/_classic2core.yaml +1 -1
- cognite/neat/_rules/transformers/__init__.py +2 -1
- cognite/neat/_rules/transformers/_converters.py +163 -61
- cognite/neat/_rules/transformers/_mapping.py +132 -2
- cognite/neat/_session/_base.py +42 -31
- cognite/neat/_session/_mapping.py +105 -5
- cognite/neat/_session/_prepare.py +43 -9
- cognite/neat/_session/_read.py +50 -4
- cognite/neat/_session/_set.py +1 -0
- cognite/neat/_session/_to.py +36 -13
- cognite/neat/_session/_wizard.py +5 -0
- cognite/neat/_session/engine/_interface.py +3 -2
- cognite/neat/_store/_base.py +79 -19
- cognite/neat/_utils/collection_.py +22 -0
- cognite/neat/_utils/rdf_.py +24 -0
- cognite/neat/_version.py +2 -2
- cognite/neat/_workflows/steps/lib/current/rules_exporter.py +3 -3
- {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.1.dist-info}/METADATA +1 -1
- {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.1.dist-info}/RECORD +47 -47
- {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.1.dist-info}/LICENSE +0 -0
- {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 =
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
412
|
-
|
|
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
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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]
|