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
@@ -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, fallback_one_by_one: 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, rules: T_VerifiedRules, client: NeatClient, dry_run: bool = False, fallback_one_by_one: bool = False
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, Sequence
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 Literal, TypeAlias, cast
5
+ from typing import Generic, Literal
5
6
 
6
- from cognite.client.data_classes._base import CogniteResource, CogniteResourceList
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
- Component: TypeAlias = Literal["all", "spaces", "data_models", "views", "containers", "node_types"]
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
- existing_handling (Literal["fail", "skip", "update", "force"], optional): How to handle existing components.
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 be updated.
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] = "all",
94
+ export_components: Component | Collection[Component] | None = None,
57
95
  include_space: set[str] | None = None,
58
- existing_handling: Literal["fail", "skip", "update", "force"] = "update",
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 = {export_components} if isinstance(export_components, str) else set(export_components)
102
+ self.export_components = export_components
63
103
  self.include_space = include_space
64
- self.existing_handling = existing_handling
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 "all" in self.export_components:
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 Inudstries in the schema
106
- return rules.as_schema(instance_space=self.instance_space, remove_cdf_spaces=True)
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
- to_export = self._prepare_exporters(rules)
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 items in reversed(to_export):
116
- loader = client.loaders.get_loader(items)
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
- deleted: set[Hashable] = set()
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
- elif to_delete:
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 CogniteAPIError as e:
143
- failed_deleted.update(loader.get_id(item) for item in e.failed + e.unknown)
144
- deleted.update(loader.get_id(item) for item in e.successful)
145
- error_messages.append(f"Failed delete: {e.message}")
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(to_delete)
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, fallback_one_by_one: bool = False
193
+ self, rules: DMSRules, client: NeatClient, dry_run: bool = False
158
194
  ) -> Iterable[UploadResult]:
159
- to_export = self._prepare_exporters(rules)
195
+ schema = self.export(rules)
160
196
 
161
- result_by_name = {}
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
- redeploy_data_model = False
167
- for items in to_export:
168
- # The conversion from DMS to GraphQL does not seem to be triggered even if the views
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
- warning_list = self._validate(loader, items, client)
179
- issue_list.extend(warning_list)
180
-
181
- created: set[Hashable] = set()
182
- skipped: set[Hashable] = set()
183
- changed: set[Hashable] = set()
184
- deleted: set[Hashable] = set()
185
- failed_created: set[Hashable] = set()
186
- failed_changed: set[Hashable] = set()
187
- failed_deleted: set[Hashable] = set()
188
- error_messages: list[str] = []
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.existing_handling in ["update", "force"]:
191
- changed.update(loader.get_id(item) for item in to_update)
192
- elif self.existing_handling == "skip":
193
- skipped.update(loader.get_id(item) for item in to_update)
194
- elif self.existing_handling == "fail":
195
- failed_changed.update(loader.get_id(item) for item in to_update)
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
- raise ValueError(f"Unsupported existing_handling {self.existing_handling}")
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 CogniteAPIError as e:
226
- if fallback_one_by_one:
227
- for item in to_create:
228
- try:
229
- loader.create([item])
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.get_id(item) for item in to_create)
241
-
242
- if self.existing_handling in ["update", "force"]:
243
- try:
244
- loader.update(to_update)
245
- except CogniteAPIError as e:
246
- if fallback_one_by_one:
247
- for item in to_update:
248
- try:
249
- loader.update([item])
250
- except CogniteAPIError as e_item:
251
- failed_changed.add(loader.get_id(item))
252
- error_messages.append(f"Failed update: {e_item!s}")
253
- else:
254
- changed.add(loader.get_id(item))
255
- else:
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
- if isinstance(items, ViewApplyList) and (created or changed):
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, loader: DataModelingLoader, items: Sequence[CogniteResource], is_redeploying
291
- ) -> tuple[list[CogniteResource], list[CogniteResource], list[CogniteResource], list[CogniteResource]]:
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
- to_create, to_update, unchanged, to_delete = [], [], [], []
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)
@@ -300,53 +304,41 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
300
304
  and not loader.in_space(item, self.include_space)
301
305
  ):
302
306
  continue
303
-
304
- cdf_item = cdf_item_by_id.get(loader.get_id(item))
307
+ item_id = loader.get_id(item)
308
+ cdf_item = cdf_item_by_id.get(item_id)
305
309
  if cdf_item is None:
306
- to_create.append(item)
307
- elif is_redeploying:
308
- to_update.append(item)
309
- to_delete.append(cdf_item)
310
+ categorized.to_create.append(item)
311
+ elif is_redeploying or self.existing == "recreate":
312
+ if not self.drop_data and loader.has_data(item_id):
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 to_create, to_delete, to_update, unchanged
320
+ categorized.to_update.append(item)
321
+ return categorized
315
322
 
316
- def _prepare_exporters(self, rules: DMSRules) -> list[CogniteResourceList]:
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 isinstance(items, DataModelApplyList):
332
- models = cast(list[DataModelApply], items)
333
- if other_models := self._exist_other_data_models(client, models):
334
- warning = PrincipleOneModelOneSpaceWarning(
335
- f"There are multiple data models in the same space {models[0].space}. "
336
- f"Other data models in the space are {other_models}.",
337
- )
338
- if not self.suppress_warnings:
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, models: list[DataModelApply]) -> list[DataModelId]:
346
- if not models:
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 = models[0].space
349
- external_id = models[0].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
- INSTANCES_OF_CLASS_QUERY = """SELECT ?s WHERE { ?s a <class> . }"""
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 (instance,) in self.graph.query( # type: ignore[misc]
163
- INSTANCES_OF_CLASS_QUERY.replace("class", class_definition["uri"])
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 INSTANCES_OF_CLASS_QUERY.replace("class", class_definition["uri"])
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()