cognite-neat 0.98.0__py3-none-any.whl → 0.99.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 (103) hide show
  1. cognite/neat/_client/__init__.py +4 -0
  2. cognite/neat/_client/_api/data_modeling_loaders.py +585 -0
  3. cognite/neat/_client/_api/schema.py +111 -0
  4. cognite/neat/_client/_api_client.py +17 -0
  5. cognite/neat/_client/data_classes/__init__.py +0 -0
  6. cognite/neat/{_utils/cdf/data_classes.py → _client/data_classes/data_modeling.py} +8 -135
  7. cognite/neat/_client/data_classes/schema.py +495 -0
  8. cognite/neat/_constants.py +27 -4
  9. cognite/neat/_graph/_shared.py +14 -15
  10. cognite/neat/_graph/extractors/_classic_cdf/_assets.py +14 -154
  11. cognite/neat/_graph/extractors/_classic_cdf/_base.py +154 -7
  12. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +25 -14
  13. cognite/neat/_graph/extractors/_classic_cdf/_data_sets.py +17 -92
  14. cognite/neat/_graph/extractors/_classic_cdf/_events.py +13 -162
  15. cognite/neat/_graph/extractors/_classic_cdf/_files.py +15 -179
  16. cognite/neat/_graph/extractors/_classic_cdf/_labels.py +32 -100
  17. cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +27 -178
  18. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +14 -139
  19. cognite/neat/_graph/extractors/_classic_cdf/_timeseries.py +15 -173
  20. cognite/neat/_graph/extractors/_rdf_file.py +6 -7
  21. cognite/neat/_graph/loaders/_rdf2dms.py +2 -2
  22. cognite/neat/_graph/queries/_base.py +17 -1
  23. cognite/neat/_graph/transformers/_classic_cdf.py +74 -147
  24. cognite/neat/_graph/transformers/_prune_graph.py +1 -1
  25. cognite/neat/_graph/transformers/_rdfpath.py +1 -1
  26. cognite/neat/_issues/_base.py +26 -17
  27. cognite/neat/_issues/errors/__init__.py +4 -2
  28. cognite/neat/_issues/errors/_external.py +7 -0
  29. cognite/neat/_issues/errors/_properties.py +2 -7
  30. cognite/neat/_issues/errors/_resources.py +1 -1
  31. cognite/neat/_issues/warnings/__init__.py +8 -0
  32. cognite/neat/_issues/warnings/_external.py +16 -0
  33. cognite/neat/_issues/warnings/_properties.py +16 -0
  34. cognite/neat/_issues/warnings/_resources.py +26 -2
  35. cognite/neat/_issues/warnings/user_modeling.py +4 -4
  36. cognite/neat/_rules/_constants.py +8 -11
  37. cognite/neat/_rules/analysis/_base.py +8 -4
  38. cognite/neat/_rules/exporters/_base.py +3 -4
  39. cognite/neat/_rules/exporters/_rules2dms.py +33 -46
  40. cognite/neat/_rules/importers/__init__.py +1 -3
  41. cognite/neat/_rules/importers/_base.py +1 -1
  42. cognite/neat/_rules/importers/_dms2rules.py +6 -29
  43. cognite/neat/_rules/importers/_rdf/__init__.py +5 -0
  44. cognite/neat/_rules/importers/_rdf/_base.py +34 -11
  45. cognite/neat/_rules/importers/_rdf/_imf2rules.py +91 -0
  46. cognite/neat/_rules/importers/_rdf/_inference2rules.py +43 -35
  47. cognite/neat/_rules/importers/_rdf/_owl2rules.py +80 -0
  48. cognite/neat/_rules/importers/_rdf/_shared.py +138 -441
  49. cognite/neat/_rules/models/__init__.py +1 -1
  50. cognite/neat/_rules/models/_base_rules.py +22 -12
  51. cognite/neat/_rules/models/dms/__init__.py +4 -2
  52. cognite/neat/_rules/models/dms/_exporter.py +45 -48
  53. cognite/neat/_rules/models/dms/_rules.py +20 -17
  54. cognite/neat/_rules/models/dms/_rules_input.py +52 -8
  55. cognite/neat/_rules/models/dms/_validation.py +391 -119
  56. cognite/neat/_rules/models/entities/_single_value.py +32 -4
  57. cognite/neat/_rules/models/information/__init__.py +2 -0
  58. cognite/neat/_rules/models/information/_rules.py +0 -67
  59. cognite/neat/_rules/models/information/_validation.py +9 -9
  60. cognite/neat/_rules/models/mapping/__init__.py +2 -3
  61. cognite/neat/_rules/models/mapping/_classic2core.py +36 -146
  62. cognite/neat/_rules/models/mapping/_classic2core.yaml +343 -0
  63. cognite/neat/_rules/transformers/__init__.py +2 -2
  64. cognite/neat/_rules/transformers/_converters.py +110 -11
  65. cognite/neat/_rules/transformers/_mapping.py +105 -30
  66. cognite/neat/_rules/transformers/_pipelines.py +1 -1
  67. cognite/neat/_rules/transformers/_verification.py +31 -3
  68. cognite/neat/_session/_base.py +24 -8
  69. cognite/neat/_session/_drop.py +35 -0
  70. cognite/neat/_session/_inspect.py +17 -5
  71. cognite/neat/_session/_mapping.py +39 -0
  72. cognite/neat/_session/_prepare.py +219 -23
  73. cognite/neat/_session/_read.py +49 -12
  74. cognite/neat/_session/_to.py +8 -5
  75. cognite/neat/_session/exceptions.py +4 -0
  76. cognite/neat/_store/_base.py +27 -24
  77. cognite/neat/_utils/rdf_.py +34 -5
  78. cognite/neat/_version.py +1 -1
  79. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +5 -88
  80. cognite/neat/_workflows/steps/lib/current/rules_importer.py +3 -14
  81. cognite/neat/_workflows/steps/lib/current/rules_validator.py +6 -7
  82. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/METADATA +3 -3
  83. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/RECORD +87 -92
  84. cognite/neat/_rules/importers/_rdf/_imf2rules/__init__.py +0 -3
  85. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2classes.py +0 -86
  86. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2metadata.py +0 -29
  87. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2properties.py +0 -130
  88. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2rules.py +0 -154
  89. cognite/neat/_rules/importers/_rdf/_owl2rules/__init__.py +0 -3
  90. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2classes.py +0 -58
  91. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2metadata.py +0 -65
  92. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2properties.py +0 -59
  93. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2rules.py +0 -39
  94. cognite/neat/_rules/models/dms/_schema.py +0 -1101
  95. cognite/neat/_rules/models/mapping/_base.py +0 -131
  96. cognite/neat/_utils/cdf/loaders/__init__.py +0 -25
  97. cognite/neat/_utils/cdf/loaders/_base.py +0 -54
  98. cognite/neat/_utils/cdf/loaders/_data_modeling.py +0 -339
  99. cognite/neat/_utils/cdf/loaders/_ingestion.py +0 -167
  100. /cognite/neat/{_utils/cdf → _client/_api}/__init__.py +0 -0
  101. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/LICENSE +0 -0
  102. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/WHEEL +0 -0
  103. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.1.dist-info}/entry_points.txt +0 -0
@@ -1,36 +1,50 @@
1
- from collections import defaultdict
2
- from typing import Any, ClassVar, cast
1
+ import warnings
2
+ from collections import Counter, defaultdict
3
+ from typing import ClassVar
3
4
 
4
5
  from cognite.client import data_modeling as dm
6
+ from cognite.client.data_classes.data_modeling import ContainerList, ViewId, ViewList
7
+ from cognite.client.data_classes.data_modeling.views import (
8
+ ReverseDirectRelation,
9
+ ReverseDirectRelationApply,
10
+ ViewProperty,
11
+ ViewPropertyApply,
12
+ )
5
13
 
6
- from cognite.neat._constants import COGNITE_MODELS, DMS_CONTAINER_PROPERTY_SIZE_LIMIT
7
- from cognite.neat._issues import IssueList, NeatError, NeatIssue, NeatIssueList
14
+ from cognite.neat._client import NeatClient
15
+ from cognite.neat._client.data_classes.data_modeling import ViewApplyDict
16
+ from cognite.neat._client.data_classes.schema import DMSSchema
17
+ from cognite.neat._constants import COGNITE_MODELS, DMS_CONTAINER_PROPERTY_SIZE_LIMIT, DMS_VIEW_CONTAINER_SIZE_LIMIT
18
+ from cognite.neat._issues import IssueList, NeatError, NeatIssueList
8
19
  from cognite.neat._issues.errors import (
20
+ CDFMissingClientError,
9
21
  PropertyDefinitionDuplicatedError,
10
- ResourceNotDefinedError,
22
+ PropertyMappingDuplicatedError,
23
+ PropertyNotFoundError,
24
+ ResourceDuplicatedError,
25
+ ResourceNotFoundError,
26
+ ReversedConnectionNotFeasibleError,
11
27
  )
12
- from cognite.neat._issues.errors._properties import ReversedConnectionNotFeasibleError
13
28
  from cognite.neat._issues.warnings import (
14
29
  NotSupportedHasDataFilterLimitWarning,
15
30
  NotSupportedViewContainerLimitWarning,
16
31
  UndefinedViewWarning,
17
32
  )
18
33
  from cognite.neat._issues.warnings.user_modeling import (
34
+ ContainerPropertyLimitWarning,
35
+ DirectRelationMissingSourceWarning,
19
36
  NotNeatSupportedFilterWarning,
20
- ViewPropertyLimitWarning,
21
37
  )
22
38
  from cognite.neat._rules.models.data_types import DataType
23
39
  from cognite.neat._rules.models.entities import ContainerEntity, RawFilter
24
40
  from cognite.neat._rules.models.entities._single_value import (
25
- ReverseConnectionEntity,
26
41
  ViewEntity,
27
42
  )
28
43
 
29
44
  from ._rules import DMSProperty, DMSRules
30
- from ._schema import DMSSchema
31
45
 
32
46
 
33
- class DMSPostValidation:
47
+ class DMSValidation:
34
48
  """This class does all the validation of the DMS rules that have dependencies between
35
49
  components."""
36
50
 
@@ -38,32 +52,132 @@ class DMSPostValidation:
38
52
  # For example, changing the filter is allowed, but changing the properties is not.
39
53
  changeable_view_attributes: ClassVar[set[str]] = {"filter"}
40
54
 
41
- def __init__(self, rules: DMSRules):
42
- self.rules = rules
43
- self.metadata = rules.metadata
44
- self.properties = rules.properties
45
- self.containers = rules.containers
46
- self.views = rules.views
47
- self.issue_list = IssueList()
55
+ def __init__(self, rules: DMSRules, client: NeatClient | None = None) -> None:
56
+ self._rules = rules
57
+ self._client = client
58
+ self._metadata = rules.metadata
59
+ self._properties = rules.properties
60
+ self._containers = rules.containers
61
+ self._views = rules.views
62
+
63
+ def imported_views_and_containers_ids(
64
+ self, include_views_with_no_properties: bool = True
65
+ ) -> tuple[set[ViewEntity], set[ContainerEntity]]:
66
+ existing_views = {view.view for view in self._views}
67
+ imported_views: set[ViewEntity] = set()
68
+ for view in self._views:
69
+ for parent in view.implements or []:
70
+ if parent not in existing_views:
71
+ imported_views.add(parent)
72
+ existing_containers = {container.container for container in self._containers or []}
73
+ imported_containers: set[ContainerEntity] = set()
74
+ view_with_properties: set[ViewEntity] = set()
75
+ for prop in self._properties:
76
+ if prop.container and prop.container not in existing_containers:
77
+ imported_containers.add(prop.container)
78
+ if prop.view not in existing_views:
79
+ imported_views.add(prop.view)
80
+ view_with_properties.add(prop.view)
81
+
82
+ if include_views_with_no_properties:
83
+ extra_views = existing_views - view_with_properties
84
+ imported_views.update({view for view in extra_views})
85
+
86
+ return imported_views, imported_containers
48
87
 
49
88
  def validate(self) -> NeatIssueList:
50
- self._validate_raw_filter()
51
- self._consistent_container_properties()
52
- self._validate_value_type_existence()
53
- self._validate_reverse_connections()
89
+ imported_views, imported_containers = self.imported_views_and_containers_ids(
90
+ include_views_with_no_properties=False
91
+ )
92
+ if (imported_views or imported_containers) and self._client is None:
93
+ raise CDFMissingClientError(
94
+ f"{self._rules.metadata.as_data_model_id()} has imported views and/or container: "
95
+ f"{imported_views}, {imported_containers}."
96
+ )
97
+ referenced_views = ViewList([])
98
+ referenced_containers = ContainerList([])
99
+ if self._client:
100
+ referenced_views = self._client.loaders.views.retrieve(
101
+ list(imported_views), include_connected=True, include_ancestor=True
102
+ )
103
+ referenced_containers = self._client.loaders.containers.retrieve(
104
+ list(imported_containers), include_connected=True
105
+ )
106
+
107
+ # Setup data structures for validation
108
+ dms_schema = self._rules.as_schema()
109
+ ref_view_by_id = {view.as_id(): view for view in referenced_views}
110
+ ref_container_by_id = {container.as_id(): container for container in referenced_containers}
111
+ all_containers_by_id: dict[dm.ContainerId, dm.ContainerApply | dm.Container] = {
112
+ **dict(dms_schema.containers.items()),
113
+ **ref_container_by_id,
114
+ }
115
+ all_views_by_id: dict[dm.ViewId, dm.ViewApply | dm.View] = {**dict(dms_schema.views.items()), **ref_view_by_id}
116
+ properties_by_ids = self._as_properties_by_ids(dms_schema, ref_view_by_id)
117
+ view_properties_by_id: dict[dm.ViewId, list[tuple[str, ViewProperty | ViewPropertyApply]]] = defaultdict(list)
118
+ for (view_id, prop_id), prop in properties_by_ids.items():
119
+ view_properties_by_id[view_id].append((prop_id, prop))
120
+
121
+ issue_list = IssueList()
122
+ # Neat DMS classes Validation
123
+ # These are errors that can only happen due to the format of the Neat DMS classes
124
+ issue_list.extend(self._validate_raw_filter())
125
+ issue_list.extend(self._consistent_container_properties())
126
+ issue_list.extend(self._validate_value_type_existence())
127
+ issue_list.extend(
128
+ self._validate_property_referenced_views_and_containers_exists(all_views_by_id, all_containers_by_id)
129
+ )
130
+
131
+ # SDK classes validation
132
+ issue_list.extend(self._containers_are_proper_size(dms_schema))
133
+ issue_list.extend(self._validate_reverse_connections(properties_by_ids, all_containers_by_id))
134
+ issue_list.extend(self._validate_schema(dms_schema, all_views_by_id, all_containers_by_id))
135
+ issue_list.extend(self._validate_referenced_container_limits(dms_schema.views, view_properties_by_id))
136
+ return issue_list
137
+
138
+ @staticmethod
139
+ def _as_properties_by_ids(
140
+ dms_schema: DMSSchema, ref_view_by_id: dict[dm.ViewId, dm.View]
141
+ ) -> dict[tuple[ViewId, str], ViewPropertyApply | ViewProperty]:
142
+ # Priority DMS schema properties.
143
+ # No need to do long lookups in ref_views as these already contain all ancestor properties.
144
+ properties_by_id: dict[tuple[ViewId, str], ViewPropertyApply | ViewProperty] = {}
145
+ for view in dms_schema.views.values():
146
+ view_id = view.as_id()
147
+ for prop_id, prop in (view.properties or {}).items():
148
+ properties_by_id[(view_id, prop_id)] = prop
149
+ if view.implements:
150
+ to_check = view.implements.copy()
151
+ while to_check:
152
+ parent_id = to_check.pop()
153
+ if parent_id in dms_schema.views:
154
+ # Priority DMS Schema properties
155
+ parent_view = dms_schema.views[parent_id]
156
+ for prop_id, prop in (parent_view.properties or {}).items():
157
+ if (view_id, prop_id) not in properties_by_id:
158
+ properties_by_id[(view_id, prop_id)] = prop
159
+ to_check.extend(parent_view.implements or [])
160
+ elif parent_id in ref_view_by_id:
161
+ # SDK properties
162
+ parent_read_view = ref_view_by_id[parent_id]
163
+ for prop_id, read_prop in parent_read_view.properties.items():
164
+ if (view_id, prop_id) not in properties_by_id:
165
+ properties_by_id[(view_id, prop_id)] = read_prop
166
+ # Read format of views already includes all ancestor properties
167
+ # so no need to check further
168
+ else:
169
+ # Missing views are caught else where
170
+ continue
54
171
 
55
- self._referenced_views_and_containers_are_existing_and_proper_size()
56
- dms_schema = self.rules.as_schema()
57
- self._validate_performance(dms_schema)
58
- return self.issue_list
172
+ return properties_by_id
59
173
 
60
- def _consistent_container_properties(self) -> None:
174
+ def _consistent_container_properties(self) -> IssueList:
61
175
  container_properties_by_id: dict[tuple[ContainerEntity, str], list[tuple[int, DMSProperty]]] = defaultdict(list)
62
- for prop_no, prop in enumerate(self.properties):
176
+ for prop_no, prop in enumerate(self._properties):
63
177
  if prop.container and prop.container_property:
64
178
  container_properties_by_id[(prop.container, prop.container_property)].append((prop_no, prop))
65
179
 
66
- errors: list[NeatError] = []
180
+ errors = IssueList()
67
181
  for (container, prop_name), properties in container_properties_by_id.items():
68
182
  if len(properties) == 1:
69
183
  continue
@@ -150,132 +264,290 @@ class DMSPostValidation:
150
264
  )
151
265
  )
152
266
 
153
- self.issue_list.extend(errors)
267
+ return errors
154
268
 
155
- def _referenced_views_and_containers_are_existing_and_proper_size(self) -> None:
156
- # TODO: Split this method and keep only validation that should be independent of
157
- # whether view and/or container exist in the pydantic model instance
158
- # other validation should be done through NeatSession.verify()
159
- defined_views = {view.view.as_id() for view in self.views}
160
-
161
- property_count_by_view: dict[dm.ViewId, int] = defaultdict(int)
162
- errors: list[NeatIssue] = []
163
- for prop_no, prop in enumerate(self.properties):
164
- view_id = prop.view.as_id()
165
- if view_id not in defined_views:
166
- errors.append(
167
- ResourceNotDefinedError[dm.ViewId](
168
- identifier=view_id,
169
- resource_type="view",
170
- location="Views Sheet",
171
- column_name="View",
172
- row_number=prop_no,
173
- sheet_name="Properties",
174
- ),
175
- )
176
- else:
177
- property_count_by_view[view_id] += 1
178
- for view_id, count in property_count_by_view.items():
269
+ @staticmethod
270
+ def _containers_are_proper_size(dms_schema: DMSSchema) -> IssueList:
271
+ errors = IssueList()
272
+ for container_id, container in dms_schema.containers.items():
273
+ count = len(container.properties or {})
179
274
  if count > DMS_CONTAINER_PROPERTY_SIZE_LIMIT:
180
- errors.append(ViewPropertyLimitWarning(view_id, count))
275
+ errors.append(ContainerPropertyLimitWarning(container_id, count))
181
276
 
182
- self.issue_list.extend(errors)
277
+ return errors
183
278
 
184
- def _validate_performance(self, dms_schema: DMSSchema) -> None:
185
- for view_id, view in dms_schema.views.items():
186
- mapped_containers = dms_schema._get_mapped_container_from_view(view_id)
279
+ @staticmethod
280
+ def _validate_referenced_container_limits(
281
+ views: ViewApplyDict, view_properties_by_id: dict[dm.ViewId, list[tuple[str, ViewProperty | ViewPropertyApply]]]
282
+ ) -> IssueList:
283
+ issue_list = IssueList()
284
+ for view_id, view in views.items():
285
+ view_properties = view_properties_by_id.get(view_id, [])
286
+ mapped_containers = {
287
+ prop.container
288
+ for _, prop in view_properties
289
+ if isinstance(prop, dm.MappedPropertyApply | dm.MappedProperty)
290
+ }
187
291
 
188
- if mapped_containers and len(mapped_containers) > 10:
189
- self.issue_list.append(
292
+ if mapped_containers and len(mapped_containers) > DMS_VIEW_CONTAINER_SIZE_LIMIT:
293
+ issue_list.append(
190
294
  NotSupportedViewContainerLimitWarning(
191
295
  view_id,
192
296
  len(mapped_containers),
193
297
  )
194
298
  )
195
- if (
196
- view.filter
197
- and isinstance(view.filter, dm.filters.HasData)
198
- and len(view.filter.dump()["hasData"]) > 10
199
- ):
200
- self.issue_list.append(
201
- NotSupportedHasDataFilterLimitWarning(
202
- view_id,
203
- len(view.filter.dump()["hasData"]),
204
- )
299
+
300
+ if view.filter and isinstance(view.filter, dm.filters.HasData) and len(view.filter.dump()["hasData"]) > 10:
301
+ issue_list.append(
302
+ NotSupportedHasDataFilterLimitWarning(
303
+ view_id,
304
+ len(view.filter.dump()["hasData"]),
205
305
  )
306
+ )
307
+ return issue_list
206
308
 
207
- def _validate_raw_filter(self) -> None:
208
- for view in self.views:
309
+ def _validate_raw_filter(self) -> IssueList:
310
+ issue_list = IssueList()
311
+ for view in self._views:
209
312
  if view.filter_ and isinstance(view.filter_, RawFilter):
210
- self.issue_list.append(
313
+ issue_list.append(
211
314
  NotNeatSupportedFilterWarning(view.view.as_id()),
212
315
  )
316
+ return issue_list
213
317
 
214
- def _validate_value_type_existence(self) -> None:
215
- views = {prop_.view for prop_ in self.properties}.union({view_.view for view_ in self.views})
216
-
217
- for prop_ in self.properties:
318
+ def _validate_value_type_existence(self) -> IssueList:
319
+ views = {prop_.view for prop_ in self._properties}.union({view_.view for view_ in self._views})
320
+ issue_list = IssueList()
321
+ for prop_ in self._properties:
218
322
  if isinstance(prop_.value_type, ViewEntity) and prop_.value_type not in views:
219
- self.issue_list.append(
323
+ issue_list.append(
220
324
  UndefinedViewWarning(
221
325
  str(prop_.view),
222
326
  str(prop_.value_type),
223
327
  prop_.view_property,
224
328
  )
225
329
  )
330
+ return issue_list
226
331
 
227
- def _validate_reverse_connections(self) -> None:
332
+ def _validate_property_referenced_views_and_containers_exists(
333
+ self,
334
+ view_by_id: dict[dm.ViewId, dm.ViewApply | dm.View],
335
+ containers_by_id: dict[dm.ContainerId, dm.ContainerApply | dm.Container],
336
+ ) -> IssueList:
337
+ issue_list = IssueList()
338
+ for prop in self._properties:
339
+ if prop.container:
340
+ container_id = prop.container.as_id()
341
+ if container_id not in containers_by_id:
342
+ issue_list.append(
343
+ ResourceNotFoundError(
344
+ container_id,
345
+ "container",
346
+ prop.view,
347
+ "view",
348
+ )
349
+ )
350
+ elif (
351
+ prop.container_property and prop.container_property not in containers_by_id[container_id].properties
352
+ ):
353
+ issue_list.append(
354
+ PropertyNotFoundError(
355
+ prop.container,
356
+ "container property",
357
+ prop.container_property,
358
+ dm.PropertyId(prop.view.as_id(), prop.view_property),
359
+ "view property",
360
+ )
361
+ )
362
+
363
+ if prop.view.as_id() not in view_by_id:
364
+ issue_list.append(
365
+ ResourceNotFoundError(
366
+ prop.view,
367
+ "view",
368
+ prop.view_property,
369
+ "property",
370
+ )
371
+ )
372
+
373
+ return issue_list
374
+
375
+ def _validate_reverse_connections(
376
+ self,
377
+ properties_by_ids: dict[tuple[dm.ViewId, str], ViewPropertyApply | ViewProperty],
378
+ containers_by_id: dict[dm.ContainerId, dm.ContainerApply | dm.Container],
379
+ ) -> IssueList:
380
+ issue_list = IssueList()
228
381
  # do not check for reverse connections in Cognite models
229
- if self.metadata.as_data_model_id() in COGNITE_MODELS:
230
- return None
231
-
232
- properties_by_ids = {f"{prop_.view!s}.{prop_.view_property}": prop_ for prop_ in self.properties}
233
- reversed_by_ids = {
234
- id_: prop_
235
- for id_, prop_ in properties_by_ids.items()
236
- if prop_.connection and isinstance(prop_.connection, ReverseConnectionEntity)
237
- }
382
+ if self._metadata.as_data_model_id() in COGNITE_MODELS:
383
+ return issue_list
238
384
 
239
- for id_, prop_ in reversed_by_ids.items():
240
- source_id = f"{prop_.value_type!s}." f"{cast(ReverseConnectionEntity, prop_.connection).property_}"
385
+ for (view_id, prop_id), prop_ in properties_by_ids.items():
386
+ if not isinstance(prop_, ReverseDirectRelationApply | ReverseDirectRelation):
387
+ continue
388
+ source_id = prop_.through.source, prop_.through.property
241
389
  if source_id not in properties_by_ids:
242
- print(f"source_id: {source_id}, first issue")
243
- self.issue_list.append(
390
+ issue_list.append(
244
391
  ReversedConnectionNotFeasibleError(
245
- id_,
392
+ view_id,
246
393
  "reversed connection",
247
- prop_.view_property,
248
- str(prop_.view),
249
- str(prop_.value_type),
250
- cast(ReverseConnectionEntity, prop_.connection).property_,
394
+ prop_id,
395
+ f"The {prop_.through.source} {prop_.through.property} does not exist",
251
396
  )
252
397
  )
398
+ continue
399
+ if isinstance(source_id[0], dm.ContainerId):
400
+ # Todo: How to handle this case? Should not happen if you created the model with Neat
401
+ continue
253
402
 
254
- elif source_id in properties_by_ids and properties_by_ids[source_id].value_type != prop_.view:
255
- print(f"source_id: {source_id}, second issue")
256
- self.issue_list.append(
403
+ target_property = properties_by_ids[(source_id[0], source_id[1])]
404
+ # Validate that the target is a direct relation pointing to the view_id
405
+ is_direct_relation = False
406
+ if isinstance(target_property, dm.MappedProperty) and isinstance(target_property.type, dm.DirectRelation):
407
+ is_direct_relation = True
408
+ elif isinstance(target_property, dm.MappedPropertyApply):
409
+ container = containers_by_id[target_property.container]
410
+ if target_property.container_property_identifier in container.properties:
411
+ container_property = container.properties[target_property.container_property_identifier]
412
+ if isinstance(container_property.type, dm.DirectRelation):
413
+ is_direct_relation = True
414
+ if not is_direct_relation:
415
+ issue_list.append(
257
416
  ReversedConnectionNotFeasibleError(
258
- id_,
259
- "view property",
260
- prop_.view_property,
261
- str(prop_.view),
262
- str(prop_.value_type),
263
- cast(ReverseConnectionEntity, prop_.connection).property_,
417
+ view_id,
418
+ "reversed connection",
419
+ prop_id,
420
+ f"{prop_.through.source} {prop_.through.property} is not a direct relation",
264
421
  )
265
422
  )
266
-
267
- else:
268
423
  continue
424
+ if not (
425
+ isinstance(target_property, dm.MappedPropertyApply | dm.MappedProperty)
426
+ and target_property.source == view_id
427
+ ):
428
+ issue_list.append(
429
+ ReversedConnectionNotFeasibleError(
430
+ view_id,
431
+ "reversed connection",
432
+ prop_id,
433
+ f"{prop_.through.source} {prop_.through.property} is not pointing to {view_id}",
434
+ )
435
+ )
436
+ return issue_list
269
437
 
270
438
  @staticmethod
271
- def _changed_attributes_and_properties(
272
- new_dumped: dict[str, Any], existing_dumped: dict[str, Any]
273
- ) -> tuple[list[str], list[str]]:
274
- """Helper method to find the changed attributes and properties between two containers or views."""
275
- new_attributes = {key: value for key, value in new_dumped.items() if key != "properties"}
276
- existing_attributes = {key: value for key, value in existing_dumped.items() if key != "properties"}
277
- changed_attributes = [key for key in new_attributes if new_attributes[key] != existing_attributes.get(key)]
278
- new_properties = new_dumped.get("properties", {})
279
- existing_properties = existing_dumped.get("properties", {})
280
- changed_properties = [prop for prop in new_properties if new_properties[prop] != existing_properties.get(prop)]
281
- return changed_attributes, changed_properties
439
+ def _validate_schema(
440
+ schema: DMSSchema,
441
+ view_by_id: dict[dm.ViewId, dm.ViewApply | dm.View],
442
+ containers_by_id: dict[dm.ContainerId, dm.ContainerApply | dm.Container],
443
+ ) -> IssueList:
444
+ errors: set[NeatError] = set()
445
+ defined_spaces = schema.spaces.copy()
446
+
447
+ for container_id, container in schema.containers.items():
448
+ if container.space not in defined_spaces:
449
+ errors.add(ResourceNotFoundError(container.space, "space", container_id, "container"))
450
+ for constraint in container.constraints.values():
451
+ if isinstance(constraint, dm.RequiresConstraint) and constraint.require not in containers_by_id:
452
+ errors.add(ResourceNotFoundError(constraint.require, "container", container_id, "container"))
453
+
454
+ for view_id, view in schema.views.items():
455
+ if view.space not in defined_spaces:
456
+ errors.add(ResourceNotFoundError(view.space, "space", view_id, "view"))
457
+
458
+ for parent in view.implements or []:
459
+ if parent not in view_by_id:
460
+ errors.add(PropertyNotFoundError(parent, "view", "implements", view_id, "view"))
461
+
462
+ for prop_name, prop in (view.properties or {}).items():
463
+ if isinstance(prop, dm.MappedPropertyApply):
464
+ ref_container = containers_by_id.get(prop.container)
465
+ if ref_container is None:
466
+ errors.add(ResourceNotFoundError(prop.container, "container", view_id, "view"))
467
+ elif prop.container_property_identifier not in ref_container.properties:
468
+ errors.add(
469
+ PropertyNotFoundError(
470
+ prop.container,
471
+ "container",
472
+ prop.container_property_identifier,
473
+ view_id,
474
+ "view",
475
+ )
476
+ )
477
+ else:
478
+ container_property = ref_container.properties[prop.container_property_identifier]
479
+
480
+ if isinstance(container_property.type, dm.DirectRelation) and prop.source is None:
481
+ warnings.warn(
482
+ DirectRelationMissingSourceWarning(view_id, prop_name),
483
+ stacklevel=2,
484
+ )
485
+
486
+ if (
487
+ isinstance(prop, dm.EdgeConnectionApply | ReverseDirectRelationApply)
488
+ and prop.source not in view_by_id
489
+ ):
490
+ errors.add(PropertyNotFoundError(prop.source, "view", prop_name, view_id, "view"))
491
+
492
+ if (
493
+ isinstance(prop, dm.EdgeConnectionApply)
494
+ and prop.edge_source is not None
495
+ and prop.edge_source not in view_by_id
496
+ ):
497
+ errors.add(PropertyNotFoundError(prop.edge_source, "view", prop_name, view_id, "view"))
498
+
499
+ # This allows for multiple view properties to be mapped to the same container property,
500
+ # as long as they have different external_id, otherwise this will lead to raising
501
+ # error ContainerPropertyUsedMultipleTimesError
502
+ property_count = Counter(
503
+ (prop.container, prop.container_property_identifier, view_property_identifier)
504
+ for view_property_identifier, prop in (view.properties or {}).items()
505
+ if isinstance(prop, dm.MappedPropertyApply)
506
+ )
507
+
508
+ for (
509
+ container_id,
510
+ container_property_identifier,
511
+ _,
512
+ ), count in property_count.items():
513
+ if count > 1:
514
+ view_properties = [
515
+ prop_name
516
+ for prop_name, prop in (view.properties or {}).items()
517
+ if isinstance(prop, dm.MappedPropertyApply)
518
+ and (prop.container, prop.container_property_identifier)
519
+ == (container_id, container_property_identifier)
520
+ ]
521
+ errors.add(
522
+ PropertyMappingDuplicatedError(
523
+ container_id,
524
+ "container",
525
+ container_property_identifier,
526
+ frozenset({dm.PropertyId(view_id, prop_name) for prop_name in view_properties}),
527
+ "view property",
528
+ )
529
+ )
530
+
531
+ if schema.data_model:
532
+ model = schema.data_model
533
+ if model.space not in defined_spaces:
534
+ errors.add(ResourceNotFoundError(model.space, "space", model.as_id(), "data model"))
535
+
536
+ view_counts: dict[dm.ViewId, int] = defaultdict(int)
537
+ for view_id_or_class in model.views or []:
538
+ view_id = view_id_or_class if isinstance(view_id_or_class, dm.ViewId) else view_id_or_class.as_id()
539
+ if view_id not in view_by_id:
540
+ errors.add(ResourceNotFoundError(view_id, "view", model.as_id(), "data model"))
541
+ view_counts[view_id] += 1
542
+
543
+ for view_id, count in view_counts.items():
544
+ if count > 1:
545
+ errors.add(
546
+ ResourceDuplicatedError(
547
+ view_id,
548
+ "view",
549
+ repr(model.as_id()),
550
+ )
551
+ )
552
+
553
+ return IssueList(list(errors))
@@ -3,7 +3,7 @@ import sys
3
3
  from abc import ABC, abstractmethod
4
4
  from functools import total_ordering
5
5
  from types import UnionType
6
- from typing import Any, ClassVar, Generic, Literal, TypeVar, Union, cast, get_args, get_origin
6
+ from typing import Any, ClassVar, Generic, Literal, TypeVar, Union, cast, get_args, get_origin, overload
7
7
 
8
8
  from cognite.client.data_classes.data_modeling import DirectRelationReference
9
9
  from cognite.client.data_classes.data_modeling.data_types import UnitReference
@@ -22,6 +22,7 @@ from pydantic import (
22
22
  model_validator,
23
23
  )
24
24
 
25
+ from cognite.neat._issues.errors import NeatValueError
25
26
  from cognite.neat._utils.text import replace_non_alphanumeric_with_underscore
26
27
 
27
28
  if sys.version_info >= (3, 11):
@@ -56,14 +57,29 @@ class Entity(BaseModel, extra="ignore"):
56
57
  suffix: str
57
58
 
58
59
  @classmethod
59
- def load(cls: "type[T_Entity]", data: Any, **defaults) -> "T_Entity | UnknownEntity":
60
+ @overload
61
+ def load(cls: "type[T_Entity]", data: Any, strict: Literal[True], **defaults) -> "T_Entity": ...
62
+
63
+ @classmethod
64
+ @overload
65
+ def load(
66
+ cls: "type[T_Entity]", data: Any, strict: Literal[False] = False, **defaults
67
+ ) -> "T_Entity | UnknownEntity": ...
68
+
69
+ @classmethod
70
+ def load(cls: "type[T_Entity]", data: Any, strict: bool = False, **defaults) -> "T_Entity | UnknownEntity":
60
71
  if isinstance(data, cls):
61
72
  return data
62
73
  elif isinstance(data, str) and data == str(Unknown):
74
+ if strict:
75
+ raise NeatValueError(f"Failed to load entity {data!s}")
63
76
  return UnknownEntity(prefix=Undefined, suffix=Unknown)
64
77
  if defaults and isinstance(defaults, dict):
65
78
  # This is a trick to pass in default values
66
- return cls.model_validate({_PARSE: data, "defaults": defaults})
79
+ try:
80
+ return cls.model_validate({_PARSE: data, "defaults": defaults})
81
+ except ValueError:
82
+ raise
67
83
  else:
68
84
  return cls.model_validate(data)
69
85
 
@@ -291,9 +307,21 @@ class DMSEntity(Entity, Generic[T_ID], ABC):
291
307
  defaults["prefix"] = defaults.pop("space")
292
308
  return super().dump(**defaults)
293
309
 
310
+ @classmethod # type: ignore[override]
311
+ @overload
312
+ def load(cls: "type[T_DMSEntity]", data: Any, strict: Literal[True], **defaults) -> "T_DMSEntity": ...
313
+
314
+ @classmethod
315
+ @overload
316
+ def load(
317
+ cls: "type[T_DMSEntity]", data: Any, strict: Literal[False] = False, **defaults
318
+ ) -> "T_DMSEntity | DMSUnknownEntity": ...
319
+
294
320
  @classmethod
295
- def load(cls: "type[T_DMSEntity]", data: Any, **defaults) -> "T_DMSEntity | DMSUnknownEntity": # type: ignore[override]
321
+ def load(cls: "type[T_DMSEntity]", data: Any, strict: bool = False, **defaults) -> "T_DMSEntity | DMSUnknownEntity": # type: ignore[override]
296
322
  if isinstance(data, str) and data == str(Unknown):
323
+ if strict:
324
+ raise NeatValueError(f"Failed to load entity {data!s}")
297
325
  return DMSUnknownEntity.from_id(None)
298
326
  return cast(T_DMSEntity, super().load(data, **defaults))
299
327