cognite-neat 0.99.1__py3-none-any.whl → 0.100.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cognite-neat might be problematic. Click here for more details.
- cognite/neat/_client/_api/data_modeling_loaders.py +383 -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 +190 -198
- 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 +34 -11
- 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.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.0.dist-info}/RECORD +47 -47
- {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.99.1.dist-info → cognite_neat-0.100.0.dist-info}/entry_points.txt +0 -0
|
@@ -101,6 +101,28 @@ class InformationAnalysis(BaseAnalysis[InformationRules, InformationClass, Infor
|
|
|
101
101
|
|
|
102
102
|
return property_renaming_configuration
|
|
103
103
|
|
|
104
|
+
def neat_id_to_transformation_property_uri(self, property_neat_id: URIRef) -> URIRef | None:
|
|
105
|
+
if (
|
|
106
|
+
(property_ := self.properties_by_neat_id.get(property_neat_id))
|
|
107
|
+
and property_.transformation
|
|
108
|
+
and isinstance(
|
|
109
|
+
property_.transformation.traversal,
|
|
110
|
+
SingleProperty,
|
|
111
|
+
)
|
|
112
|
+
and (
|
|
113
|
+
property_.transformation.traversal.property.prefix in self.rules.prefixes
|
|
114
|
+
or property_.transformation.traversal.property.prefix == self.rules.metadata.prefix
|
|
115
|
+
)
|
|
116
|
+
):
|
|
117
|
+
namespace = (
|
|
118
|
+
self.rules.metadata.namespace
|
|
119
|
+
if property_.transformation.traversal.property.prefix == self.rules.metadata.prefix
|
|
120
|
+
else self.rules.prefixes[property_.transformation.traversal.property.prefix]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return namespace[property_.transformation.traversal.property.suffix]
|
|
124
|
+
return None
|
|
125
|
+
|
|
104
126
|
def property_types(self, class_: ClassEntity) -> dict[str, EntityTypes]:
|
|
105
127
|
property_types = {}
|
|
106
128
|
if definitions := self.class_property_pairs(consider_inheritance=True).get(class_, None):
|
|
@@ -31,11 +31,9 @@ class BaseExporter(ABC, Generic[T_VerifiedRules, T_Export]):
|
|
|
31
31
|
class CDFExporter(BaseExporter[T_VerifiedRules, T_Export]):
|
|
32
32
|
@abstractmethod
|
|
33
33
|
def export_to_cdf_iterable(
|
|
34
|
-
self, rules: T_VerifiedRules, client: NeatClient, dry_run: bool = False
|
|
34
|
+
self, rules: T_VerifiedRules, client: NeatClient, dry_run: bool = False
|
|
35
35
|
) -> Iterable[UploadResult]:
|
|
36
36
|
raise NotImplementedError
|
|
37
37
|
|
|
38
|
-
def export_to_cdf(
|
|
39
|
-
self
|
|
40
|
-
) -> UploadResultList:
|
|
41
|
-
return UploadResultList(self.export_to_cdf_iterable(rules, client, dry_run, fallback_one_by_one))
|
|
38
|
+
def export_to_cdf(self, rules: T_VerifiedRules, client: NeatClient, dry_run: bool = False) -> UploadResultList:
|
|
39
|
+
return UploadResultList(self.export_to_cdf_iterable(rules, client, dry_run))
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import warnings
|
|
2
|
-
from collections.abc import Collection, Hashable, Iterable
|
|
2
|
+
from collections.abc import Callable, Collection, Hashable, Iterable
|
|
3
|
+
from dataclasses import dataclass, field
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import Generic, Literal
|
|
5
6
|
|
|
6
|
-
from cognite.client.data_classes._base import
|
|
7
|
+
from cognite.client.data_classes._base import (
|
|
8
|
+
T_CogniteResourceList,
|
|
9
|
+
T_WritableCogniteResource,
|
|
10
|
+
T_WriteClass,
|
|
11
|
+
)
|
|
7
12
|
from cognite.client.data_classes.data_modeling import (
|
|
8
|
-
ContainerApplyList,
|
|
9
|
-
DataModelApply,
|
|
10
13
|
DataModelApplyList,
|
|
11
14
|
DataModelId,
|
|
12
|
-
SpaceApplyList,
|
|
13
15
|
ViewApplyList,
|
|
14
16
|
)
|
|
15
17
|
from cognite.client.exceptions import CogniteAPIError
|
|
16
18
|
|
|
17
19
|
from cognite.neat._client import DataModelingLoader, NeatClient
|
|
20
|
+
from cognite.neat._client._api.data_modeling_loaders import MultiCogniteAPIError, T_WritableCogniteResourceList
|
|
21
|
+
from cognite.neat._client.data_classes.data_modeling import Component
|
|
18
22
|
from cognite.neat._client.data_classes.schema import DMSSchema
|
|
19
23
|
from cognite.neat._issues import IssueList
|
|
20
24
|
from cognite.neat._issues.warnings import (
|
|
@@ -22,11 +26,44 @@ from cognite.neat._issues.warnings import (
|
|
|
22
26
|
ResourceRetrievalWarning,
|
|
23
27
|
)
|
|
24
28
|
from cognite.neat._rules.models.dms import DMSRules
|
|
29
|
+
from cognite.neat._shared import T_ID
|
|
25
30
|
from cognite.neat._utils.upload import UploadResult
|
|
26
31
|
|
|
27
32
|
from ._base import CDFExporter
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ItemCategorized(Generic[T_ID, T_WriteClass]):
|
|
37
|
+
resource_name: str
|
|
38
|
+
as_id: Callable[[T_WriteClass], T_ID]
|
|
39
|
+
to_create: list[T_WriteClass] = field(default_factory=list)
|
|
40
|
+
to_update: list[T_WriteClass] = field(default_factory=list)
|
|
41
|
+
to_delete: list[T_WriteClass] = field(default_factory=list)
|
|
42
|
+
to_skip: list[T_WriteClass] = field(default_factory=list)
|
|
43
|
+
unchanged: list[T_WriteClass] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def to_create_ids(self) -> list[T_ID]:
|
|
47
|
+
return [self.as_id(item) for item in self.to_create]
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def to_update_ids(self) -> list[T_ID]:
|
|
51
|
+
return [self.as_id(item) for item in self.to_update]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def to_skip_ids(self) -> list[T_ID]:
|
|
55
|
+
return [self.as_id(item) for item in self.to_skip]
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def to_delete_ids(self) -> list[T_ID]:
|
|
59
|
+
return [self.as_id(item) for item in self.to_delete]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def unchanged_ids(self) -> list[T_ID]:
|
|
63
|
+
return [self.as_id(item) for item in self.unchanged]
|
|
64
|
+
|
|
65
|
+
def item_ids(self) -> Iterable[T_ID]:
|
|
66
|
+
yield from (self.as_id(item) for item in self.to_create + self.to_update + self.to_delete + self.unchanged)
|
|
30
67
|
|
|
31
68
|
|
|
32
69
|
class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
|
|
@@ -37,34 +74,39 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
|
|
|
37
74
|
Which components to export. Defaults to frozenset({"all"}).
|
|
38
75
|
include_space (set[str], optional):
|
|
39
76
|
If set, only export components in the given spaces. Defaults to None which means all spaces.
|
|
40
|
-
|
|
77
|
+
existing (Literal["fail", "skip", "update", "force"], optional): How to handle existing components.
|
|
41
78
|
Defaults to "update". See below for details.
|
|
42
79
|
instance_space (str, optional): The space to use for the instance. Defaults to None.
|
|
43
80
|
suppress_warnings (bool, optional): Suppress warnings. Defaults to False.
|
|
81
|
+
remove_cdf_spaces (bool, optional): Skip views and containers that are system are in system spaces.
|
|
44
82
|
|
|
45
83
|
... note::
|
|
46
84
|
|
|
47
85
|
- "fail": If any component already exists, the export will fail.
|
|
48
86
|
- "skip": If any component already exists, it will be skipped.
|
|
49
|
-
- "update": If any component already exists, it will
|
|
87
|
+
- "update": If any component already exists, it will
|
|
50
88
|
- "force": If any component already exists, it will be deleted and recreated.
|
|
51
89
|
|
|
52
90
|
"""
|
|
53
91
|
|
|
54
92
|
def __init__(
|
|
55
93
|
self,
|
|
56
|
-
export_components: Component | Collection[Component] =
|
|
94
|
+
export_components: Component | Collection[Component] | None = None,
|
|
57
95
|
include_space: set[str] | None = None,
|
|
58
|
-
|
|
96
|
+
existing: Literal["fail", "skip", "update", "force", "recreate"] = "update",
|
|
59
97
|
instance_space: str | None = None,
|
|
60
98
|
suppress_warnings: bool = False,
|
|
99
|
+
drop_data: bool = False,
|
|
100
|
+
remove_cdf_spaces: bool = True,
|
|
61
101
|
):
|
|
62
|
-
self.export_components =
|
|
102
|
+
self.export_components = export_components
|
|
63
103
|
self.include_space = include_space
|
|
64
|
-
self.
|
|
104
|
+
self.existing = existing
|
|
105
|
+
self.drop_data = drop_data
|
|
65
106
|
self.instance_space = instance_space
|
|
66
107
|
self.suppress_warnings = suppress_warnings
|
|
67
108
|
self._schema: DMSSchema | None = None
|
|
109
|
+
self.remove_cdf_spaces = remove_cdf_spaces
|
|
68
110
|
|
|
69
111
|
def export_to_file(self, rules: DMSRules, filepath: Path) -> None:
|
|
70
112
|
"""Export the rules to a file(s).
|
|
@@ -95,30 +137,28 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
|
|
|
95
137
|
schema.to_zip(filepath, exclude=exclude)
|
|
96
138
|
|
|
97
139
|
def _create_exclude_set(self):
|
|
98
|
-
if
|
|
140
|
+
if self.export_components is None:
|
|
99
141
|
exclude = set()
|
|
100
142
|
else:
|
|
101
|
-
exclude = {"spaces", "data_models", "views", "containers", "node_types"} - self.export_components
|
|
143
|
+
exclude = {"spaces", "data_models", "views", "containers", "node_types"} - set(self.export_components)
|
|
102
144
|
return exclude
|
|
103
145
|
|
|
104
146
|
def export(self, rules: DMSRules) -> DMSSchema:
|
|
105
|
-
# We do not want to include CogniteCore/CogniteProcess
|
|
106
|
-
return rules.as_schema(instance_space=self.instance_space, remove_cdf_spaces=
|
|
147
|
+
# We do not want to include CogniteCore/CogniteProcess Industries in the schema
|
|
148
|
+
return rules.as_schema(instance_space=self.instance_space, remove_cdf_spaces=self.remove_cdf_spaces)
|
|
107
149
|
|
|
108
150
|
def delete_from_cdf(
|
|
109
151
|
self, rules: DMSRules, client: NeatClient, dry_run: bool = False, skip_space: bool = False
|
|
110
152
|
) -> Iterable[UploadResult]:
|
|
111
|
-
|
|
153
|
+
schema = self.export(rules)
|
|
112
154
|
|
|
113
155
|
# we need to reverse order in which we are picking up the items to delete
|
|
114
156
|
# as they are sorted in the order of creation and we need to delete them in reverse order
|
|
115
|
-
for
|
|
116
|
-
|
|
117
|
-
if skip_space and isinstance(items, SpaceApplyList):
|
|
118
|
-
continue
|
|
157
|
+
for loader in reversed(client.loaders.by_dependency_order(self.export_components)):
|
|
158
|
+
items = loader.items_from_schema(schema)
|
|
119
159
|
item_ids = loader.get_ids(items)
|
|
120
160
|
existing_items = loader.retrieve(item_ids)
|
|
121
|
-
existing_ids = loader.get_ids(existing_items)
|
|
161
|
+
existing_ids = set(loader.get_ids(existing_items))
|
|
122
162
|
to_delete: list[Hashable] = []
|
|
123
163
|
for item_id in item_ids:
|
|
124
164
|
if (
|
|
@@ -131,168 +171,132 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
|
|
|
131
171
|
if item_id in existing_ids:
|
|
132
172
|
to_delete.append(item_id)
|
|
133
173
|
|
|
134
|
-
|
|
135
|
-
failed_deleted: set[Hashable] = set()
|
|
136
|
-
error_messages: list[str] = []
|
|
174
|
+
result = UploadResult(loader.resource_name) # type: ignore[var-annotated]
|
|
137
175
|
if dry_run:
|
|
138
|
-
deleted.update(to_delete)
|
|
139
|
-
|
|
176
|
+
result.deleted.update(to_delete)
|
|
177
|
+
yield result
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if to_delete:
|
|
140
181
|
try:
|
|
141
|
-
loader.delete(to_delete)
|
|
142
|
-
except
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
182
|
+
deleted = loader.delete(to_delete)
|
|
183
|
+
except MultiCogniteAPIError as e:
|
|
184
|
+
result.deleted.update([loader.get_id(item) for item in e.success])
|
|
185
|
+
result.failed_deleted.update([loader.get_id(item) for item in e.failed])
|
|
186
|
+
for error in e.errors:
|
|
187
|
+
result.error_messages.append(f"Failed to delete {loader.resource_name}: {error!s}")
|
|
146
188
|
else:
|
|
147
|
-
deleted.update(
|
|
148
|
-
|
|
149
|
-
yield UploadResult(
|
|
150
|
-
name=loader.resource_name,
|
|
151
|
-
deleted=deleted,
|
|
152
|
-
failed_deleted=failed_deleted,
|
|
153
|
-
error_messages=error_messages,
|
|
154
|
-
)
|
|
189
|
+
result.deleted.update(deleted)
|
|
190
|
+
yield result
|
|
155
191
|
|
|
156
192
|
def export_to_cdf_iterable(
|
|
157
|
-
self, rules: DMSRules, client: NeatClient, dry_run: bool = False
|
|
193
|
+
self, rules: DMSRules, client: NeatClient, dry_run: bool = False
|
|
158
194
|
) -> Iterable[UploadResult]:
|
|
159
|
-
|
|
195
|
+
schema = self.export(rules)
|
|
160
196
|
|
|
161
|
-
|
|
162
|
-
if self.existing_handling == "force":
|
|
163
|
-
for delete_result in self.delete_from_cdf(rules, client, dry_run, skip_space=True):
|
|
164
|
-
result_by_name[delete_result.name] = delete_result
|
|
197
|
+
categorized_items_by_loader = self._categorize_by_loader(client, schema)
|
|
165
198
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
# are changed. This is a workaround to force the conversion.
|
|
170
|
-
is_redeploying = isinstance(items, DataModelApplyList) and redeploy_data_model
|
|
171
|
-
loader = client.loaders.get_loader(items)
|
|
172
|
-
|
|
173
|
-
to_create, to_delete, to_update, unchanged = self._categorize_items_for_upload(
|
|
174
|
-
loader, items, is_redeploying
|
|
175
|
-
)
|
|
199
|
+
is_failing = self.existing == "fail" and any(
|
|
200
|
+
loader.resource_name for loader, categorized in categorized_items_by_loader.items() if categorized.to_update
|
|
201
|
+
)
|
|
176
202
|
|
|
203
|
+
for loader, items in categorized_items_by_loader.items():
|
|
177
204
|
issue_list = IssueList()
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
205
|
+
|
|
206
|
+
if items.resource_name == client.loaders.data_models.resource_name:
|
|
207
|
+
warning_list = self._validate(list(items.item_ids()), client)
|
|
208
|
+
issue_list.extend(warning_list)
|
|
209
|
+
|
|
210
|
+
results = UploadResult(loader.resource_name, issues=issue_list) # type: ignore[var-annotated]
|
|
211
|
+
if is_failing:
|
|
212
|
+
# If any component already exists, the export will fail.
|
|
213
|
+
# This is the same if we run dry_run or not.
|
|
214
|
+
results.failed_upserted.update(items.to_update_ids)
|
|
215
|
+
results.failed_created.update(items.to_create_ids)
|
|
216
|
+
results.failed_deleted.update(items.to_delete_ids)
|
|
217
|
+
results.unchanged.update(items.unchanged_ids)
|
|
218
|
+
results.error_messages.append("Existing components found and existing_handling is 'fail'")
|
|
219
|
+
yield results
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
results.unchanged.update(items.unchanged_ids)
|
|
223
|
+
results.skipped.update(items.to_skip_ids)
|
|
189
224
|
if dry_run:
|
|
190
|
-
if self.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
225
|
+
if self.existing in ["update", "force"]:
|
|
226
|
+
# Assume all changed are successful
|
|
227
|
+
results.changed.update(items.to_update_ids)
|
|
228
|
+
elif self.existing == "skip":
|
|
229
|
+
results.skipped.update(items.to_update_ids)
|
|
230
|
+
results.deleted.update(items.to_delete_ids)
|
|
231
|
+
results.created.update(items.to_create_ids)
|
|
232
|
+
yield results
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if items.to_delete_ids:
|
|
236
|
+
try:
|
|
237
|
+
deleted = loader.delete(items.to_delete_ids)
|
|
238
|
+
except MultiCogniteAPIError as e:
|
|
239
|
+
results.deleted.update([loader.get_id(item) for item in e.success])
|
|
240
|
+
results.failed_deleted.update([loader.get_id(item) for item in e.failed])
|
|
241
|
+
for error in e.errors:
|
|
242
|
+
results.error_messages.append(f"Failed to delete {loader.resource_name}: {error!s}")
|
|
196
243
|
else:
|
|
197
|
-
|
|
198
|
-
created.update(loader.get_id(item) for item in to_create)
|
|
199
|
-
deleted.update(loader.get_id(item) for item in to_delete)
|
|
200
|
-
else:
|
|
201
|
-
if to_delete:
|
|
202
|
-
try:
|
|
203
|
-
loader.delete(to_delete)
|
|
204
|
-
except CogniteAPIError as e:
|
|
205
|
-
if fallback_one_by_one:
|
|
206
|
-
for item in to_delete:
|
|
207
|
-
try:
|
|
208
|
-
loader.delete([item])
|
|
209
|
-
except CogniteAPIError as item_e:
|
|
210
|
-
failed_deleted.add(loader.get_id(item))
|
|
211
|
-
error_messages.append(f"Failed delete: {item_e!s}")
|
|
212
|
-
else:
|
|
213
|
-
deleted.add(loader.get_id(item))
|
|
214
|
-
else:
|
|
215
|
-
error_messages.append(f"Failed delete: {e!s}")
|
|
216
|
-
failed_deleted.update(loader.get_id(item) for item in e.failed + e.unknown)
|
|
217
|
-
else:
|
|
218
|
-
deleted.update(loader.get_id(item) for item in to_delete)
|
|
219
|
-
|
|
220
|
-
if isinstance(items, DataModelApplyList):
|
|
221
|
-
to_create = loader.sort_by_dependencies(to_create)
|
|
244
|
+
results.deleted.update(deleted)
|
|
222
245
|
|
|
246
|
+
if items.to_create:
|
|
223
247
|
try:
|
|
224
|
-
loader.create(to_create)
|
|
225
|
-
except
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
except CogniteAPIError as item_e:
|
|
231
|
-
failed_created.add(loader.get_id(item))
|
|
232
|
-
error_messages.append(f"Failed create: {item_e!s}")
|
|
233
|
-
else:
|
|
234
|
-
created.add(loader.get_id(item))
|
|
235
|
-
else:
|
|
236
|
-
failed_created.update(loader.get_id(item) for item in e.failed + e.unknown)
|
|
237
|
-
created.update(loader.get_id(item) for item in e.successful)
|
|
238
|
-
error_messages.append(f"Failed create: {e!s}")
|
|
248
|
+
created = loader.create(items.to_create)
|
|
249
|
+
except MultiCogniteAPIError as e:
|
|
250
|
+
results.created.update([loader.get_id(item) for item in e.success])
|
|
251
|
+
results.failed_created.update([loader.get_id(item) for item in e.failed])
|
|
252
|
+
for error in e.errors:
|
|
253
|
+
results.error_messages.append(f"Failed to create {loader.resource_name}: {error!s}")
|
|
239
254
|
else:
|
|
240
|
-
created.update(loader.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
failed_changed.update(loader.get_id(item) for item in e.failed + e.unknown)
|
|
257
|
-
changed.update(loader.get_id(item) for item in e.successful)
|
|
258
|
-
error_messages.append(f"Failed update: {e!s}")
|
|
259
|
-
else:
|
|
260
|
-
changed.update(loader.get_id(item) for item in to_update)
|
|
261
|
-
elif self.existing_handling == "skip":
|
|
262
|
-
skipped.update(loader.get_id(item) for item in to_update)
|
|
263
|
-
elif self.existing_handling == "fail":
|
|
264
|
-
failed_changed.update(loader.get_id(item) for item in to_update)
|
|
265
|
-
|
|
266
|
-
if loader.resource_name in result_by_name:
|
|
267
|
-
delete_result = result_by_name[loader.resource_name]
|
|
268
|
-
deleted.update(delete_result.deleted)
|
|
269
|
-
failed_deleted.update(delete_result.failed_deleted)
|
|
270
|
-
error_messages.extend(delete_result.error_messages)
|
|
271
|
-
|
|
272
|
-
yield UploadResult(
|
|
273
|
-
name=loader.resource_name,
|
|
274
|
-
created=created,
|
|
275
|
-
changed=changed,
|
|
276
|
-
deleted=deleted,
|
|
277
|
-
unchanged={loader.get_id(item) for item in unchanged},
|
|
278
|
-
skipped=skipped,
|
|
279
|
-
failed_created=failed_created,
|
|
280
|
-
failed_changed=failed_changed,
|
|
281
|
-
failed_deleted=failed_deleted,
|
|
282
|
-
error_messages=error_messages,
|
|
283
|
-
issues=issue_list,
|
|
284
|
-
)
|
|
255
|
+
results.created.update(loader.get_ids(created))
|
|
256
|
+
|
|
257
|
+
if items.to_update and self.existing == "skip":
|
|
258
|
+
results.skipped.update(items.to_update_ids)
|
|
259
|
+
elif items.to_update:
|
|
260
|
+
try:
|
|
261
|
+
updated = loader.update(items.to_update, force=self.existing == "force", drop_data=self.drop_data)
|
|
262
|
+
except MultiCogniteAPIError as e:
|
|
263
|
+
results.changed.update([loader.get_id(item) for item in e.success])
|
|
264
|
+
results.failed_changed.update([loader.get_id(item) for item in e.failed])
|
|
265
|
+
for error in e.errors:
|
|
266
|
+
results.error_messages.append(f"Failed to update {loader.resource_name}: {error!s}")
|
|
267
|
+
else:
|
|
268
|
+
results.changed.update(loader.get_ids(updated))
|
|
269
|
+
|
|
270
|
+
yield results
|
|
285
271
|
|
|
286
|
-
|
|
272
|
+
def _categorize_by_loader(self, client: NeatClient, schema: DMSSchema) -> dict[DataModelingLoader, ItemCategorized]:
|
|
273
|
+
categorized_items_by_loader: dict[DataModelingLoader, ItemCategorized] = {}
|
|
274
|
+
redeploy_data_model = False
|
|
275
|
+
for loader in client.loaders.by_dependency_order(self.export_components):
|
|
276
|
+
items = loader.items_from_schema(schema)
|
|
277
|
+
# The conversion from DMS to GraphQL does not seem to be triggered even if the views
|
|
278
|
+
# are changed. This is a workaround to force the conversion.
|
|
279
|
+
is_redeploying = isinstance(items, DataModelApplyList) and redeploy_data_model
|
|
280
|
+
|
|
281
|
+
categorized = self._categorize_items_for_upload(loader, items, is_redeploying)
|
|
282
|
+
categorized_items_by_loader[loader] = categorized
|
|
283
|
+
|
|
284
|
+
if isinstance(items, ViewApplyList) and (categorized.to_create or categorized.to_update):
|
|
287
285
|
redeploy_data_model = True
|
|
286
|
+
return categorized_items_by_loader
|
|
288
287
|
|
|
289
288
|
def _categorize_items_for_upload(
|
|
290
|
-
self,
|
|
291
|
-
|
|
289
|
+
self,
|
|
290
|
+
loader: DataModelingLoader[
|
|
291
|
+
T_ID, T_WriteClass, T_WritableCogniteResource, T_CogniteResourceList, T_WritableCogniteResourceList
|
|
292
|
+
],
|
|
293
|
+
items: T_CogniteResourceList,
|
|
294
|
+
is_redeploying: bool,
|
|
295
|
+
) -> ItemCategorized[T_ID, T_WriteClass]:
|
|
292
296
|
item_ids = loader.get_ids(items)
|
|
293
297
|
cdf_items = loader.retrieve(item_ids)
|
|
294
298
|
cdf_item_by_id = {loader.get_id(item): item for item in cdf_items}
|
|
295
|
-
|
|
299
|
+
categorized = ItemCategorized[T_ID, T_WriteClass](loader.resource_name, loader.get_id)
|
|
296
300
|
for item in items:
|
|
297
301
|
if (
|
|
298
302
|
isinstance(items, DataModelApplyList)
|
|
@@ -303,50 +307,38 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
|
|
|
303
307
|
|
|
304
308
|
cdf_item = cdf_item_by_id.get(loader.get_id(item))
|
|
305
309
|
if cdf_item is None:
|
|
306
|
-
to_create.append(item)
|
|
307
|
-
elif is_redeploying:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
+
categorized.to_create.append(item)
|
|
311
|
+
elif is_redeploying or self.existing == "recreate":
|
|
312
|
+
if loader.has_data(cdf_item) and not self.drop_data:
|
|
313
|
+
categorized.to_skip.append(cdf_item)
|
|
314
|
+
else:
|
|
315
|
+
categorized.to_delete.append(cdf_item.as_write())
|
|
316
|
+
categorized.to_create.append(item)
|
|
310
317
|
elif loader.are_equal(item, cdf_item):
|
|
311
|
-
unchanged.append(item)
|
|
318
|
+
categorized.unchanged.append(item)
|
|
312
319
|
else:
|
|
313
|
-
to_update.append(item)
|
|
314
|
-
return
|
|
320
|
+
categorized.to_update.append(item)
|
|
321
|
+
return categorized
|
|
315
322
|
|
|
316
|
-
def
|
|
317
|
-
schema = self.export(rules)
|
|
318
|
-
to_export: list[CogniteResourceList] = []
|
|
319
|
-
if self.export_components.intersection({"all", "spaces"}):
|
|
320
|
-
to_export.append(SpaceApplyList(schema.spaces.values()))
|
|
321
|
-
if self.export_components.intersection({"all", "containers"}):
|
|
322
|
-
to_export.append(ContainerApplyList(schema.containers.values()))
|
|
323
|
-
if self.export_components.intersection({"all", "views"}):
|
|
324
|
-
to_export.append(ViewApplyList(schema.views.values()))
|
|
325
|
-
if self.export_components.intersection({"all", "data_models"}):
|
|
326
|
-
to_export.append(DataModelApplyList([schema.data_model]))
|
|
327
|
-
return to_export
|
|
328
|
-
|
|
329
|
-
def _validate(self, loader: DataModelingLoader, items: CogniteResourceList, client: NeatClient) -> IssueList:
|
|
323
|
+
def _validate(self, items: list[DataModelId], client: NeatClient) -> IssueList:
|
|
330
324
|
issue_list = IssueList()
|
|
331
|
-
if
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
warnings.warn(warning, stacklevel=2)
|
|
340
|
-
issue_list.append(warning)
|
|
325
|
+
if other_models := self._exist_other_data_models(client, items):
|
|
326
|
+
warning = PrincipleOneModelOneSpaceWarning(
|
|
327
|
+
f"There are multiple data models in the same space {items[0].space}. "
|
|
328
|
+
f"Other data models in the space are {other_models}.",
|
|
329
|
+
)
|
|
330
|
+
if not self.suppress_warnings:
|
|
331
|
+
warnings.warn(warning, stacklevel=2)
|
|
332
|
+
issue_list.append(warning)
|
|
341
333
|
|
|
342
334
|
return issue_list
|
|
343
335
|
|
|
344
336
|
@classmethod
|
|
345
|
-
def _exist_other_data_models(cls, client: NeatClient,
|
|
346
|
-
if not
|
|
337
|
+
def _exist_other_data_models(cls, client: NeatClient, model_ids: list[DataModelId]) -> list[DataModelId]:
|
|
338
|
+
if not model_ids:
|
|
347
339
|
return []
|
|
348
|
-
space =
|
|
349
|
-
external_id =
|
|
340
|
+
space = model_ids[0].space
|
|
341
|
+
external_id = model_ids[0].external_id
|
|
350
342
|
try:
|
|
351
343
|
data_models = client.data_modeling.data_models.list(space=space, limit=25, all_versions=False)
|
|
352
344
|
except CogniteAPIError as e:
|
|
@@ -26,7 +26,14 @@ ORDERED_CLASSES_QUERY = """SELECT ?class (count(?s) as ?instances )
|
|
|
26
26
|
WHERE { ?s a ?class . }
|
|
27
27
|
group by ?class order by DESC(?instances)"""
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
INSTANCES_OF_CLASS_QUERY = """SELECT ?s ?propertyCount WHERE { ?s a <class> . BIND ('Unknown' as ?propertyCount) }"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
INSTANCES_OF_CLASS_RICHNESS_ORDERED_QUERY = """SELECT ?s (COUNT(?p) as ?propertyCount)
|
|
34
|
+
WHERE { ?s a <class> ; ?p ?o . }
|
|
35
|
+
GROUP BY ?s
|
|
36
|
+
ORDER BY DESC(?propertyCount)"""
|
|
30
37
|
|
|
31
38
|
INSTANCE_PROPERTIES_DEFINITION = """SELECT ?property (count(?property) as ?occurrence) ?dataType ?objectType
|
|
32
39
|
WHERE {<instance_id> ?property ?value .
|
|
@@ -157,13 +164,19 @@ class InferenceImporter(BaseRDFImporter):
|
|
|
157
164
|
|
|
158
165
|
self._add_uri_namespace_to_prefixes(cast(URIRef, class_uri), prefixes)
|
|
159
166
|
|
|
167
|
+
instances_query = (
|
|
168
|
+
INSTANCES_OF_CLASS_QUERY if self.max_number_of_instance == -1 else INSTANCES_OF_CLASS_RICHNESS_ORDERED_QUERY
|
|
169
|
+
)
|
|
170
|
+
|
|
160
171
|
# Infers all the properties of the class
|
|
161
172
|
for class_id, class_definition in classes.items():
|
|
162
|
-
for (
|
|
163
|
-
|
|
173
|
+
for ( # type: ignore[misc]
|
|
174
|
+
instance,
|
|
175
|
+
_,
|
|
176
|
+
) in self.graph.query( # type: ignore[misc]
|
|
177
|
+
instances_query.replace("class", class_definition["uri"])
|
|
164
178
|
if self.max_number_of_instance < 0
|
|
165
|
-
else
|
|
166
|
-
+ f" LIMIT {self.max_number_of_instance}"
|
|
179
|
+
else instances_query.replace("class", class_definition["uri"]) + f" LIMIT {self.max_number_of_instance}"
|
|
167
180
|
):
|
|
168
181
|
for property_uri, occurrence, data_type_uri, object_type_uri in self.graph.query( # type: ignore[misc]
|
|
169
182
|
INSTANCE_PROPERTIES_DEFINITION.replace("instance_id", instance)
|
|
@@ -242,6 +255,10 @@ class InferenceImporter(BaseRDFImporter):
|
|
|
242
255
|
|
|
243
256
|
# Create multi-value properties otherwise single value
|
|
244
257
|
for property_ in properties.values():
|
|
258
|
+
# Removes non-existing node type from value type prior final conversion to string
|
|
259
|
+
if len(property_["value_type"]) > 1 and str(self.non_existing_node_type) in property_["value_type"]:
|
|
260
|
+
property_["value_type"].remove(str(self.non_existing_node_type))
|
|
261
|
+
|
|
245
262
|
if len(property_["value_type"]) > 1:
|
|
246
263
|
property_["value_type"] = " | ".join([str(t) for t in property_["value_type"]])
|
|
247
264
|
else:
|
|
@@ -5,6 +5,7 @@ its sub-models and validators.
|
|
|
5
5
|
import math
|
|
6
6
|
import sys
|
|
7
7
|
import types
|
|
8
|
+
import uuid
|
|
8
9
|
from abc import ABC, abstractmethod
|
|
9
10
|
from collections.abc import Callable, Hashable, Iterator, MutableSequence, Sequence
|
|
10
11
|
from datetime import datetime
|
|
@@ -30,6 +31,7 @@ from pydantic import (
|
|
|
30
31
|
PlainSerializer,
|
|
31
32
|
field_validator,
|
|
32
33
|
model_serializer,
|
|
34
|
+
model_validator,
|
|
33
35
|
)
|
|
34
36
|
from pydantic.main import IncEx
|
|
35
37
|
from pydantic_core import core_schema
|
|
@@ -42,6 +44,7 @@ from cognite.neat._rules.models._types import (
|
|
|
42
44
|
DmsPropertyType,
|
|
43
45
|
SpaceType,
|
|
44
46
|
StrListType,
|
|
47
|
+
URIRefType,
|
|
45
48
|
VersionType,
|
|
46
49
|
ViewEntityType,
|
|
47
50
|
)
|
|
@@ -331,7 +334,23 @@ class BaseRules(SchemaModel, ABC):
|
|
|
331
334
|
return output
|
|
332
335
|
|
|
333
336
|
|
|
337
|
+
def make_neat_id() -> URIRef:
|
|
338
|
+
return DEFAULT_NAMESPACE[f"neatId_{str(uuid.uuid4()).replace('-', '_')}"]
|
|
339
|
+
|
|
340
|
+
|
|
334
341
|
class SheetRow(SchemaModel):
|
|
342
|
+
neatId: URIRefType | None = Field(
|
|
343
|
+
alias="Neat ID",
|
|
344
|
+
description="Globally unique identifier for the property",
|
|
345
|
+
default=None,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
@model_validator(mode="after")
|
|
349
|
+
def set_neat_id(self) -> "SheetRow":
|
|
350
|
+
if self.neatId is None:
|
|
351
|
+
self.neatId = DEFAULT_NAMESPACE[f"neatId_{str(uuid.uuid4()).replace('-', '_')}"]
|
|
352
|
+
return self
|
|
353
|
+
|
|
335
354
|
@abstractmethod
|
|
336
355
|
def _identifier(self) -> tuple[Hashable, ...]:
|
|
337
356
|
raise NotImplementedError()
|