cognite-neat 0.99.0__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.

Files changed (84) hide show
  1. cognite/neat/_client/_api/data_modeling_loaders.py +390 -116
  2. cognite/neat/_client/_api/schema.py +63 -2
  3. cognite/neat/_client/data_classes/data_modeling.py +4 -0
  4. cognite/neat/_client/data_classes/schema.py +2 -348
  5. cognite/neat/_constants.py +27 -4
  6. cognite/neat/_graph/extractors/_base.py +7 -0
  7. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +28 -18
  8. cognite/neat/_graph/loaders/_rdf2dms.py +52 -13
  9. cognite/neat/_graph/transformers/__init__.py +3 -3
  10. cognite/neat/_graph/transformers/_classic_cdf.py +135 -56
  11. cognite/neat/_issues/_base.py +26 -17
  12. cognite/neat/_issues/errors/__init__.py +4 -2
  13. cognite/neat/_issues/errors/_external.py +7 -0
  14. cognite/neat/_issues/errors/_properties.py +2 -7
  15. cognite/neat/_issues/errors/_resources.py +1 -1
  16. cognite/neat/_issues/warnings/__init__.py +6 -2
  17. cognite/neat/_issues/warnings/_external.py +9 -1
  18. cognite/neat/_issues/warnings/_resources.py +41 -2
  19. cognite/neat/_issues/warnings/user_modeling.py +4 -4
  20. cognite/neat/_rules/_constants.py +2 -6
  21. cognite/neat/_rules/analysis/_base.py +15 -5
  22. cognite/neat/_rules/analysis/_dms.py +20 -0
  23. cognite/neat/_rules/analysis/_information.py +22 -0
  24. cognite/neat/_rules/exporters/_base.py +3 -5
  25. cognite/neat/_rules/exporters/_rules2dms.py +190 -200
  26. cognite/neat/_rules/importers/__init__.py +1 -3
  27. cognite/neat/_rules/importers/_base.py +1 -1
  28. cognite/neat/_rules/importers/_dms2rules.py +3 -25
  29. cognite/neat/_rules/importers/_rdf/__init__.py +5 -0
  30. cognite/neat/_rules/importers/_rdf/_base.py +34 -11
  31. cognite/neat/_rules/importers/_rdf/_imf2rules.py +91 -0
  32. cognite/neat/_rules/importers/_rdf/_inference2rules.py +40 -7
  33. cognite/neat/_rules/importers/_rdf/_owl2rules.py +80 -0
  34. cognite/neat/_rules/importers/_rdf/_shared.py +138 -441
  35. cognite/neat/_rules/models/_base_rules.py +19 -0
  36. cognite/neat/_rules/models/_types.py +5 -0
  37. cognite/neat/_rules/models/dms/__init__.py +2 -0
  38. cognite/neat/_rules/models/dms/_exporter.py +247 -123
  39. cognite/neat/_rules/models/dms/_rules.py +7 -49
  40. cognite/neat/_rules/models/dms/_rules_input.py +8 -3
  41. cognite/neat/_rules/models/dms/_validation.py +421 -123
  42. cognite/neat/_rules/models/entities/_multi_value.py +3 -0
  43. cognite/neat/_rules/models/information/__init__.py +2 -0
  44. cognite/neat/_rules/models/information/_rules.py +17 -61
  45. cognite/neat/_rules/models/information/_rules_input.py +11 -2
  46. cognite/neat/_rules/models/information/_validation.py +107 -11
  47. cognite/neat/_rules/models/mapping/_classic2core.py +1 -1
  48. cognite/neat/_rules/models/mapping/_classic2core.yaml +8 -4
  49. cognite/neat/_rules/transformers/__init__.py +2 -1
  50. cognite/neat/_rules/transformers/_converters.py +163 -61
  51. cognite/neat/_rules/transformers/_mapping.py +132 -2
  52. cognite/neat/_rules/transformers/_pipelines.py +1 -1
  53. cognite/neat/_rules/transformers/_verification.py +29 -4
  54. cognite/neat/_session/_base.py +46 -60
  55. cognite/neat/_session/_mapping.py +105 -5
  56. cognite/neat/_session/_prepare.py +49 -14
  57. cognite/neat/_session/_read.py +50 -4
  58. cognite/neat/_session/_set.py +1 -0
  59. cognite/neat/_session/_to.py +38 -12
  60. cognite/neat/_session/_wizard.py +5 -0
  61. cognite/neat/_session/engine/_interface.py +3 -2
  62. cognite/neat/_session/exceptions.py +4 -0
  63. cognite/neat/_store/_base.py +79 -19
  64. cognite/neat/_utils/collection_.py +22 -0
  65. cognite/neat/_utils/rdf_.py +30 -4
  66. cognite/neat/_version.py +2 -2
  67. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +3 -91
  68. cognite/neat/_workflows/steps/lib/current/rules_importer.py +2 -16
  69. cognite/neat/_workflows/steps/lib/current/rules_validator.py +3 -5
  70. {cognite_neat-0.99.0.dist-info → cognite_neat-0.100.0.dist-info}/METADATA +1 -1
  71. {cognite_neat-0.99.0.dist-info → cognite_neat-0.100.0.dist-info}/RECORD +74 -82
  72. cognite/neat/_rules/importers/_rdf/_imf2rules/__init__.py +0 -3
  73. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2classes.py +0 -86
  74. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2metadata.py +0 -29
  75. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2properties.py +0 -130
  76. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2rules.py +0 -154
  77. cognite/neat/_rules/importers/_rdf/_owl2rules/__init__.py +0 -3
  78. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2classes.py +0 -58
  79. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2metadata.py +0 -65
  80. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2properties.py +0 -59
  81. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2rules.py +0 -39
  82. {cognite_neat-0.99.0.dist-info → cognite_neat-0.100.0.dist-info}/LICENSE +0 -0
  83. {cognite_neat-0.99.0.dist-info → cognite_neat-0.100.0.dist-info}/WHEEL +0 -0
  84. {cognite_neat-0.99.0.dist-info → cognite_neat-0.100.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  import warnings
2
2
  from collections import defaultdict
3
3
  from collections.abc import Collection, Hashable, Sequence
4
- from typing import Any
4
+ from typing import Any, cast
5
5
 
6
6
  from cognite.client.data_classes import data_modeling as dm
7
7
  from cognite.client.data_classes.data_modeling.containers import BTreeIndex
@@ -20,7 +20,8 @@ from cognite.neat._client.data_classes.data_modeling import (
20
20
  ViewApplyDict,
21
21
  )
22
22
  from cognite.neat._client.data_classes.schema import DMSSchema
23
- from cognite.neat._issues.errors import NeatTypeError, ResourceNotFoundError
23
+ from cognite.neat._constants import COGNITE_SPACES
24
+ from cognite.neat._issues.errors import NeatTypeError, NeatValueError, ResourceNotFoundError
24
25
  from cognite.neat._issues.warnings import NotSupportedWarning, PropertyNotFoundWarning
25
26
  from cognite.neat._issues.warnings.user_modeling import (
26
27
  EmptyContainerWarning,
@@ -54,19 +55,13 @@ class _DMSExporter:
54
55
  include_pipeline (bool): If True, the pipeline will be included with the schema. Pipeline means the
55
56
  raw tables and transformations necessary to populate the data model.
56
57
  instance_space (str): The space to use for the instance. Defaults to None,`Rules.metadata.space` will be used
58
+ remove_cdf_spaces(bool): The
57
59
  """
58
60
 
59
- def __init__(self, rules: DMSRules, instance_space: str | None = None):
61
+ def __init__(self, rules: DMSRules, instance_space: str | None = None, remove_cdf_spaces: bool = False):
60
62
  self.instance_space = instance_space
61
63
  self.rules = rules
62
- self._ref_schema = None
63
- if self._ref_schema:
64
- # We skip version as that will always be missing in the reference
65
- self._ref_views_by_id = {
66
- dm.ViewId(view.space, view.external_id): view for view in self._ref_schema.views.values()
67
- }
68
- else:
69
- self._ref_views_by_id = {} # type: ignore
64
+ self.remove_cdf_spaces = remove_cdf_spaces
70
65
 
71
66
  def to_schema(self) -> DMSSchema:
72
67
  rules = self.rules
@@ -78,7 +73,7 @@ class _DMSExporter:
78
73
  view_properties_by_id, rules.views
79
74
  )
80
75
 
81
- views = self._create_views_with_node_types(view_properties_by_id, view_properties_with_ancestors_by_id)
76
+ views = self._create_views(view_properties_by_id, view_properties_with_ancestors_by_id)
82
77
  view_node_type_filters: set[dm.NodeId] = set()
83
78
  for dms_view in rules.views:
84
79
  if isinstance(dms_view.filter_, NodeTypeFilter):
@@ -89,21 +84,22 @@ class _DMSExporter:
89
84
  + [dm.NodeApply(node.space, node.external_id) for node in view_node_type_filters]
90
85
  )
91
86
  else:
92
- node_types = NodeApplyDict([dm.NodeApply(node.space, node.external_id) for node in view_node_type_filters])
93
-
94
- last_schema: DMSSchema | None = None
87
+ node_types = NodeApplyDict(
88
+ [
89
+ dm.NodeApply(node.space, node.external_id)
90
+ for node in view_node_type_filters
91
+ if not (self.remove_cdf_spaces and node.space in COGNITE_SPACES)
92
+ ]
93
+ )
95
94
 
96
- views_not_in_model = {view.view.as_id() for view in rules.views if not view.in_model}
97
95
  data_model = rules.metadata.as_data_model()
98
-
99
- data_model_views = [view_id for view_id in views if view_id not in views_not_in_model]
100
-
101
96
  # Sorting to ensure deterministic order
102
- data_model.views = sorted(data_model_views, key=lambda v: v.as_tuple()) # type: ignore[union-attr]
103
-
97
+ data_model.views = sorted(
98
+ [dms_view.view.as_id() for dms_view in rules.views if dms_view.in_model],
99
+ key=lambda x: x.as_tuple(), # type: ignore[union-attr]
100
+ )
104
101
  spaces = self._create_spaces(rules.metadata, containers, views, data_model)
105
-
106
- output = DMSSchema(
102
+ return DMSSchema(
107
103
  spaces=spaces,
108
104
  data_model=data_model,
109
105
  views=views,
@@ -111,12 +107,6 @@ class _DMSExporter:
111
107
  node_types=node_types,
112
108
  )
113
109
 
114
- if self._ref_schema:
115
- output.reference = self._ref_schema
116
- if last_schema:
117
- output.last = last_schema
118
- return output
119
-
120
110
  def _create_spaces(
121
111
  self,
122
112
  metadata: DMSMetadata,
@@ -136,21 +126,34 @@ class _DMSExporter:
136
126
  spaces[self.instance_space] = dm.SpaceApply(space=self.instance_space, name=self.instance_space)
137
127
  return spaces
138
128
 
139
- def _create_views_with_node_types(
129
+ def _create_views(
140
130
  self,
141
131
  view_properties_by_id: dict[dm.ViewId, list[DMSProperty]],
142
132
  view_properties_with_ancestors_by_id: dict[dm.ViewId, list[DMSProperty]],
143
133
  ) -> ViewApplyDict:
144
134
  input_views = list(self.rules.views)
145
135
 
146
- views = ViewApplyDict([dms_view.as_view() for dms_view in input_views])
136
+ views = ViewApplyDict(
137
+ [
138
+ dms_view.as_view()
139
+ for dms_view in input_views
140
+ if not (self.remove_cdf_spaces and dms_view.view.space in COGNITE_SPACES)
141
+ ]
142
+ )
143
+ view_by_id = {dms_view.view: dms_view for dms_view in input_views}
144
+
145
+ edge_types_by_view_property_id = self._edge_types_by_view_property_id(
146
+ view_properties_with_ancestors_by_id, view_by_id
147
+ )
147
148
 
148
149
  for view_id, view in views.items():
149
150
  view.properties = {}
150
151
  if not (view_properties := view_properties_by_id.get(view_id)):
151
152
  continue
152
153
  for prop in view_properties:
153
- view_property = self._create_view_property(prop, view_properties_with_ancestors_by_id)
154
+ view_property = self._create_view_property(
155
+ prop, view_properties_with_ancestors_by_id, edge_types_by_view_property_id
156
+ )
154
157
  if view_property is not None:
155
158
  view.properties[prop.view_property] = view_property
156
159
 
@@ -161,18 +164,112 @@ class _DMSExporter:
161
164
  if isinstance(prop.connection, EdgeEntity) and prop.connection.edge_type is not None:
162
165
  return prop.connection.edge_type.as_reference()
163
166
  elif isinstance(prop.value_type, ViewEntity):
164
- return cls._create_edge_type_from_view_id(prop.view.as_id(), prop.view_property)
167
+ return cls._default_edge_type_from_view_id(prop.view.as_id(), prop.view_property)
165
168
  else:
166
169
  raise NeatTypeError(f"Invalid valueType {prop.value_type!r}")
167
170
 
168
171
  @staticmethod
169
- def _create_edge_type_from_view_id(view_id: dm.ViewId, property_: str) -> dm.DirectRelationReference:
172
+ def _default_edge_type_from_view_id(view_id: dm.ViewId, property_: str) -> dm.DirectRelationReference:
170
173
  return dm.DirectRelationReference(
171
174
  space=view_id.space,
172
175
  # This is the same convention as used when converting GraphQL to DMS
173
176
  external_id=f"{view_id.external_id}.{property_}",
174
177
  )
175
178
 
179
+ @classmethod
180
+ def _edge_types_by_view_property_id(
181
+ cls,
182
+ view_properties_with_ancestors_by_id: dict[dm.ViewId, list[DMSProperty]],
183
+ view_by_id: dict[ViewEntity, DMSView],
184
+ ) -> dict[tuple[ViewEntity, str], dm.DirectRelationReference]:
185
+ edge_connection_property_by_view_property_id: dict[tuple[ViewEntity, str], DMSProperty] = {}
186
+ for properties in view_properties_with_ancestors_by_id.values():
187
+ for prop in properties:
188
+ if isinstance(prop.connection, EdgeEntity):
189
+ view_property_id = (prop.view, prop.view_property)
190
+ edge_connection_property_by_view_property_id[view_property_id] = prop
191
+
192
+ edge_types_by_view_property_id: dict[tuple[ViewEntity, str], dm.DirectRelationReference] = {}
193
+
194
+ outwards_type_by_view_value_type: dict[tuple[ViewEntity, ViewEntity], list[dm.DirectRelationReference]] = (
195
+ defaultdict(list)
196
+ )
197
+ # First set the edge types for outwards connections.
198
+ for (view_id, _), prop in edge_connection_property_by_view_property_id.items():
199
+ # We have already filtered out all non-EdgeEntity connections
200
+ connection = cast(EdgeEntity, prop.connection)
201
+ if connection.direction == "inwards":
202
+ continue
203
+ view = view_by_id[view_id]
204
+
205
+ edge_type = cls._get_edge_type_outwards_connection(
206
+ view, prop, view_by_id, edge_connection_property_by_view_property_id
207
+ )
208
+
209
+ edge_types_by_view_property_id[(prop.view, prop.view_property)] = edge_type
210
+
211
+ if isinstance(prop.value_type, ViewEntity):
212
+ outwards_type_by_view_value_type[(prop.value_type, prop.view)].append(edge_type)
213
+
214
+ # Then inwards connections = outwards connections
215
+ for (view_id, prop_id), prop in edge_connection_property_by_view_property_id.items():
216
+ # We have already filtered out all non-EdgeEntity connections
217
+ connection = cast(EdgeEntity, prop.connection)
218
+
219
+ if connection.direction == "inwards" and isinstance(prop.value_type, ViewEntity):
220
+ edge_type_candidates = outwards_type_by_view_value_type.get((prop.view, prop.value_type), [])
221
+ if len(edge_type_candidates) == 0:
222
+ # Warning in validation, should not have an inwards connection without an outwards connection
223
+ edge_type = cls._default_edge_type_from_view_id(prop.view.as_id(), prop_id)
224
+ elif len(edge_type_candidates) == 1:
225
+ edge_type = edge_type_candidates[0]
226
+ else:
227
+ raise NeatValueError(
228
+ f"Cannot infer edge type for {view_id}.{prop_id}, multiple candidates: {edge_type_candidates}."
229
+ "Please specify edge type explicitly, i.e., edge(type=<YOUR_TYPE>)."
230
+ )
231
+ view_property_id = (prop.view, prop.view_property)
232
+ edge_types_by_view_property_id[view_property_id] = edge_type
233
+
234
+ return edge_types_by_view_property_id
235
+
236
+ @classmethod
237
+ def _get_edge_type_outwards_connection(
238
+ cls,
239
+ view: DMSView,
240
+ prop: DMSProperty,
241
+ view_by_id: dict[ViewEntity, DMSView],
242
+ edge_connection_by_view_property_id: dict[tuple[ViewEntity, str], DMSProperty],
243
+ ) -> dm.DirectRelationReference:
244
+ connection = cast(EdgeEntity, prop.connection)
245
+ if connection.edge_type is not None:
246
+ # Explicitly set edge type
247
+ return connection.edge_type.as_reference()
248
+ elif view.implements:
249
+ # Try to look for same property in parent views
250
+ candidates = []
251
+ for parent_id in view.implements:
252
+ if parent_view := view_by_id.get(parent_id):
253
+ parent_prop = edge_connection_by_view_property_id.get((parent_view.view, prop.view_property))
254
+ if parent_prop and isinstance(parent_prop.connection, EdgeEntity):
255
+ parent_edge_type = cls._get_edge_type_outwards_connection(
256
+ parent_view, parent_prop, view_by_id, edge_connection_by_view_property_id
257
+ )
258
+ candidates.append(parent_edge_type)
259
+ if len(candidates) == 0:
260
+ return cls._default_edge_type_from_view_id(prop.view.as_id(), prop.view_property)
261
+ elif len(candidates) == 1:
262
+ return candidates[0]
263
+ else:
264
+ raise NeatValueError(
265
+ f"Cannot infer edge type for {prop.view.as_id()!r}.{prop.view_property}, "
266
+ f"multiple candidates: {candidates}. "
267
+ "Please specify edge type explicitly, i.e., edge(type=<YOUR_TYPE>)."
268
+ )
269
+ else:
270
+ # No parent view, use the default
271
+ return cls._default_edge_type_from_view_id(prop.view.as_id(), prop.view_property)
272
+
176
273
  def _create_containers(
177
274
  self,
178
275
  container_properties_by_id: dict[dm.ContainerId, list[DMSProperty]],
@@ -184,7 +281,13 @@ class _DMSExporter:
184
281
 
185
282
  containers = list(self.rules.containers or [])
186
283
 
187
- containers = dm.ContainerApplyList([dms_container.as_container() for dms_container in containers])
284
+ containers = dm.ContainerApplyList(
285
+ [
286
+ dms_container.as_container()
287
+ for dms_container in containers
288
+ if not (self.remove_cdf_spaces and dms_container.container.space in COGNITE_SPACES)
289
+ ]
290
+ )
188
291
  container_to_drop = set()
189
292
  for container in containers:
190
293
  container_id = container.as_id()
@@ -371,106 +474,127 @@ class _DMSExporter:
371
474
 
372
475
  @classmethod
373
476
  def _create_view_property(
374
- cls, prop: DMSProperty, view_properties_with_ancestors_by_id: dict[dm.ViewId, list[DMSProperty]]
477
+ cls,
478
+ prop: DMSProperty,
479
+ view_properties_with_ancestors_by_id: dict[dm.ViewId, list[DMSProperty]],
480
+ edge_types_by_view_property_id: dict[tuple[ViewEntity, str], dm.DirectRelationReference],
375
481
  ) -> ViewPropertyApply | None:
376
482
  if prop.container and prop.container_property:
377
- container_prop_identifier = prop.container_property
378
- extra_args: dict[str, Any] = {}
379
- if prop.connection == "direct":
380
- if isinstance(prop.value_type, ViewEntity):
381
- extra_args["source"] = prop.value_type.as_id()
382
- elif isinstance(prop.value_type, DMSUnknownEntity):
383
- extra_args["source"] = None
384
- else:
385
- # Should have been validated.
386
- raise ValueError(
387
- "If this error occurs it is a bug in NEAT, please report"
388
- f"Debug Info, Invalid valueType direct: {prop.model_dump_json()}"
389
- )
390
- elif prop.connection is not None:
391
- # Should have been validated.
392
- raise ValueError(
393
- "If this error occurs it is a bug in NEAT, please report"
394
- f"Debug Info, Invalid connection: {prop.model_dump_json()}"
395
- )
396
- return dm.MappedPropertyApply(
397
- container=prop.container.as_id(),
398
- container_property_identifier=container_prop_identifier,
399
- name=prop.name,
400
- description=prop.description,
401
- **extra_args,
402
- )
483
+ return cls._create_mapped_property(prop)
403
484
  elif isinstance(prop.connection, EdgeEntity):
404
- if isinstance(prop.value_type, ViewEntity):
405
- source_view_id = prop.value_type.as_id()
406
- else:
407
- # Should have been validated.
408
- raise ValueError(
409
- "If this error occurs it is a bug in NEAT, please report"
410
- f"Debug Info, Invalid valueType edge: {prop.model_dump_json()}"
411
- )
412
- edge_source: dm.ViewId | None = None
413
- if prop.connection.properties is not None:
414
- edge_source = prop.connection.properties.as_id()
415
- edge_cls: type[dm.EdgeConnectionApply] = dm.MultiEdgeConnectionApply
416
- # If is_list is not set, we default to a MultiEdgeConnection
417
- if prop.is_list is False:
418
- edge_cls = SingleEdgeConnectionApply
419
-
420
- return edge_cls(
421
- type=cls._create_edge_type_from_prop(prop),
422
- source=source_view_id,
423
- direction=prop.connection.direction,
424
- name=prop.name,
425
- description=prop.description,
426
- edge_source=edge_source,
427
- )
485
+ return cls._create_edge_property(prop, edge_types_by_view_property_id)
428
486
  elif isinstance(prop.connection, ReverseConnectionEntity):
429
- reverse_prop_id = prop.connection.property_
487
+ return cls._create_reverse_direct_relation(prop, view_properties_with_ancestors_by_id)
488
+ elif prop.view and prop.view_property and prop.connection:
489
+ warnings.warn(
490
+ NotSupportedWarning(f"{prop.connection} in {prop.view.as_id()!r}.{prop.view_property}"), stacklevel=2
491
+ )
492
+ return None
493
+
494
+ @classmethod
495
+ def _create_mapped_property(cls, prop: DMSProperty) -> dm.MappedPropertyApply:
496
+ container = cast(ContainerEntity, prop.container)
497
+ container_prop_identifier = cast(str, prop.container_property)
498
+ extra_args: dict[str, Any] = {}
499
+ if prop.connection == "direct":
430
500
  if isinstance(prop.value_type, ViewEntity):
431
- source_view_id = prop.value_type.as_id()
501
+ extra_args["source"] = prop.value_type.as_id()
502
+ elif isinstance(prop.value_type, DMSUnknownEntity):
503
+ extra_args["source"] = None
432
504
  else:
433
505
  # Should have been validated.
434
506
  raise ValueError(
435
507
  "If this error occurs it is a bug in NEAT, please report"
436
- f"Debug Info, Invalid valueType reverse connection: {prop.model_dump_json()}"
508
+ f"Debug Info, Invalid valueType direct: {prop.model_dump_json()}"
437
509
  )
438
- reverse_prop = next(
439
- (
440
- prop
441
- for prop in view_properties_with_ancestors_by_id.get(source_view_id, [])
442
- if prop.view_property == reverse_prop_id
443
- ),
444
- None,
510
+ elif prop.connection is not None:
511
+ # Should have been validated.
512
+ raise ValueError(
513
+ "If this error occurs it is a bug in NEAT, please report"
514
+ f"Debug Info, Invalid connection: {prop.model_dump_json()}"
445
515
  )
446
- if reverse_prop is None:
447
- warnings.warn(
448
- PropertyNotFoundWarning(
449
- source_view_id,
450
- "view",
451
- reverse_prop_id or "MISSING",
452
- dm.PropertyId(prop.view.as_id(), prop.view_property),
453
- "view property",
454
- ),
455
- stacklevel=2,
456
- )
516
+ return dm.MappedPropertyApply(
517
+ container=container.as_id(),
518
+ container_property_identifier=container_prop_identifier,
519
+ name=prop.name,
520
+ description=prop.description,
521
+ **extra_args,
522
+ )
457
523
 
458
- if reverse_prop and reverse_prop.connection == "direct":
459
- args: dict[str, Any] = dict(
460
- source=source_view_id,
461
- through=dm.PropertyId(source=source_view_id, property=reverse_prop_id),
462
- name=prop.name,
463
- description=prop.description,
464
- )
465
- if prop.is_list in [True, None]:
466
- return dm.MultiReverseDirectRelationApply(**args)
467
- else:
468
- return SingleReverseDirectRelationApply(**args)
469
- else:
470
- return None
524
+ @classmethod
525
+ def _create_edge_property(
526
+ cls, prop: DMSProperty, edge_types_by_view_property_id: dict[tuple[ViewEntity, str], dm.DirectRelationReference]
527
+ ) -> dm.EdgeConnectionApply:
528
+ connection = cast(EdgeEntity, prop.connection)
529
+ if isinstance(prop.value_type, ViewEntity):
530
+ source_view_id = prop.value_type.as_id()
531
+ else:
532
+ # Should have been validated.
533
+ raise ValueError(
534
+ "If this error occurs it is a bug in NEAT, please report"
535
+ f"Debug Info, Invalid valueType edge: {prop.model_dump_json()}"
536
+ )
537
+ edge_source: dm.ViewId | None = None
538
+ if connection.properties is not None:
539
+ edge_source = connection.properties.as_id()
540
+ edge_cls: type[dm.EdgeConnectionApply] = dm.MultiEdgeConnectionApply
541
+ # If is_list is not set, we default to a MultiEdgeConnection
542
+ if prop.is_list is False:
543
+ edge_cls = SingleEdgeConnectionApply
544
+
545
+ return edge_cls(
546
+ type=edge_types_by_view_property_id[(prop.view, prop.view_property)],
547
+ source=source_view_id,
548
+ direction=connection.direction,
549
+ name=prop.name,
550
+ description=prop.description,
551
+ edge_source=edge_source,
552
+ )
471
553
 
472
- elif prop.view and prop.view_property and prop.connection:
554
+ @classmethod
555
+ def _create_reverse_direct_relation(
556
+ cls, prop: DMSProperty, view_properties_with_ancestors_by_id: dict[dm.ViewId, list[DMSProperty]]
557
+ ) -> dm.MultiReverseDirectRelationApply | SingleReverseDirectRelationApply | None:
558
+ connection = cast(ReverseConnectionEntity, prop.connection)
559
+ reverse_prop_id = connection.property_
560
+ if isinstance(prop.value_type, ViewEntity):
561
+ source_view_id = prop.value_type.as_id()
562
+ else:
563
+ # Should have been validated.
564
+ raise ValueError(
565
+ "If this error occurs it is a bug in NEAT, please report"
566
+ f"Debug Info, Invalid valueType reverse connection: {prop.model_dump_json()}"
567
+ )
568
+ reverse_prop = next(
569
+ (
570
+ prop
571
+ for prop in view_properties_with_ancestors_by_id.get(source_view_id, [])
572
+ if prop.view_property == reverse_prop_id
573
+ ),
574
+ None,
575
+ )
576
+ if reverse_prop is None:
473
577
  warnings.warn(
474
- NotSupportedWarning(f"{prop.connection} in {prop.view.as_id()!r}.{prop.view_property}"), stacklevel=2
578
+ PropertyNotFoundWarning(
579
+ source_view_id,
580
+ "view",
581
+ reverse_prop_id or "MISSING",
582
+ dm.PropertyId(prop.view.as_id(), prop.view_property),
583
+ "view property",
584
+ ),
585
+ stacklevel=2,
475
586
  )
476
- return None
587
+
588
+ if reverse_prop and reverse_prop.connection == "direct":
589
+ args: dict[str, Any] = dict(
590
+ source=source_view_id,
591
+ through=dm.PropertyId(source=source_view_id, property=reverse_prop_id),
592
+ name=prop.name,
593
+ description=prop.description,
594
+ )
595
+ if prop.is_list in [True, None]:
596
+ return dm.MultiReverseDirectRelationApply(**args)
597
+ else:
598
+ return SingleReverseDirectRelationApply(**args)
599
+ else:
600
+ return None
@@ -4,13 +4,11 @@ from typing import Any, ClassVar, Literal
4
4
 
5
5
  import pandas as pd
6
6
  from cognite.client import data_modeling as dm
7
- from pydantic import Field, field_serializer, field_validator, model_validator
7
+ from pydantic import Field, field_serializer, field_validator
8
8
  from pydantic_core.core_schema import SerializationInfo, ValidationInfo
9
- from rdflib import URIRef
10
9
 
11
10
  from cognite.neat._client.data_classes.schema import DMSSchema
12
11
  from cognite.neat._constants import COGNITE_SPACES
13
- from cognite.neat._issues import MultiValueError
14
12
  from cognite.neat._issues.errors import NeatValueError
15
13
  from cognite.neat._issues.warnings import (
16
14
  PrincipleMatchingSpaceAndVersionWarning,
@@ -31,6 +29,7 @@ from cognite.neat._rules.models._types import (
31
29
  ContainerEntityType,
32
30
  DmsPropertyType,
33
31
  StrListType,
32
+ URIRefType,
34
33
  ViewEntityType,
35
34
  )
36
35
  from cognite.neat._rules.models.data_types import DataType
@@ -55,7 +54,7 @@ _DEFAULT_VERSION = "1"
55
54
  class DMSMetadata(BaseMetadata):
56
55
  role: ClassVar[RoleTypes] = RoleTypes.dms
57
56
  aspect: ClassVar[DataModelAspect] = DataModelAspect.physical
58
- logical: str | None = None
57
+ logical: URIRefType | None = None
59
58
 
60
59
  def as_space(self) -> dm.SpaceApply:
61
60
  return dm.SpaceApply(
@@ -109,7 +108,7 @@ class DMSProperty(SheetRow):
109
108
  container_property: DmsPropertyType | None = Field(None, alias="Container Property")
110
109
  index: StrListType | None = Field(None, alias="Index")
111
110
  constraint: StrListType | None = Field(None, alias="Constraint")
112
- logical: URIRef | None = Field(
111
+ logical: URIRefType | None = Field(
113
112
  None,
114
113
  alias="Logical",
115
114
  description="Used to make connection between physical and logical data model aspect",
@@ -247,7 +246,7 @@ class DMSView(SheetRow):
247
246
  implements: ViewEntityList | None = Field(None, alias="Implements")
248
247
  filter_: HasDataFilter | NodeTypeFilter | RawFilter | None = Field(None, alias="Filter")
249
248
  in_model: bool = Field(True, alias="In Model")
250
- logical: URIRef | None = Field(
249
+ logical: URIRefType | None = Field(
251
250
  None,
252
251
  alias="Logical",
253
252
  description="Used to make connection between physical and logical data model aspect",
@@ -339,9 +338,6 @@ class DMSRules(BaseRules):
339
338
  containers: SheetList[DMSContainer] | None = Field(None, alias="Containers")
340
339
  enum: SheetList[DMSEnum] | None = Field(None, alias="Enum")
341
340
  nodes: SheetList[DMSNode] | None = Field(None, alias="Nodes")
342
- # This is a hack to allow the post_validation to be turned off when needed
343
- # Will likely be moved completely out of the rules in the future
344
- post_validate: bool = Field(default=True, exclude=True, repr=False)
345
341
 
346
342
  @field_validator("views")
347
343
  def matching_version_and_space(cls, value: SheetList[DMSView], info: ValidationInfo) -> SheetList[DMSView]:
@@ -374,23 +370,10 @@ class DMSRules(BaseRules):
374
370
  )
375
371
  return value
376
372
 
377
- @model_validator(mode="after")
378
- def post_validation(self) -> "DMSRules":
379
- from ._validation import DMSPostValidation
380
-
381
- if not self.post_validate:
382
- return self
383
- issue_list = DMSPostValidation(self).validate()
384
- if issue_list.warnings:
385
- issue_list.trigger_warnings()
386
- if issue_list.has_errors:
387
- raise MultiValueError(issue_list.errors)
388
- return self
389
-
390
- def as_schema(self, include_pipeline: bool = False, instance_space: str | None = None) -> DMSSchema:
373
+ def as_schema(self, instance_space: str | None = None, remove_cdf_spaces: bool = False) -> DMSSchema:
391
374
  from ._exporter import _DMSExporter
392
375
 
393
- return _DMSExporter(self, instance_space).to_schema()
376
+ return _DMSExporter(self, instance_space, remove_cdf_spaces=remove_cdf_spaces).to_schema()
394
377
 
395
378
  def _repr_html_(self) -> str:
396
379
  summary = {
@@ -406,28 +389,3 @@ class DMSRules(BaseRules):
406
389
  }
407
390
 
408
391
  return pd.DataFrame([summary]).T.rename(columns={0: ""})._repr_html_() # type: ignore
409
-
410
- def imported_views_and_containers_ids(
411
- self, include_model_views_with_no_properties: bool = True
412
- ) -> tuple[set[dm.ViewId], set[dm.ContainerId]]:
413
- existing_views = {view.view for view in self.views}
414
- imported_views: set[dm.ViewId] = set()
415
- for view in self.views:
416
- for parent in view.implements or []:
417
- if parent not in existing_views:
418
- imported_views.add(parent.as_id())
419
- existing_containers = {container.container for container in self.containers or []}
420
- imported_containers: set[dm.ContainerId] = set()
421
- view_with_properties: set[ViewEntity] = set()
422
- for prop in self.properties:
423
- if prop.container and prop.container not in existing_containers:
424
- imported_containers.add(prop.container.as_id())
425
- if prop.view not in existing_views:
426
- imported_views.add(prop.view.as_id())
427
- view_with_properties.add(prop.view)
428
-
429
- if include_model_views_with_no_properties:
430
- extra_views = existing_views - view_with_properties
431
- imported_views.update({view.as_id() for view in extra_views})
432
-
433
- return imported_views, imported_containers
@@ -35,7 +35,7 @@ class DMSInputMetadata(InputComponent[DMSMetadata]):
35
35
  description: str | None = None
36
36
  created: datetime | str | None = None
37
37
  updated: datetime | str | None = None
38
- logical: str | None = None
38
+ logical: str | URIRef | None = None
39
39
 
40
40
  @classmethod
41
41
  def _get_verified_cls(cls) -> type[DMSMetadata]:
@@ -108,7 +108,8 @@ class DMSInputProperty(InputComponent[DMSProperty]):
108
108
  container_property: str | None = None
109
109
  index: str | list[str] | None = None
110
110
  constraint: str | list[str] | None = None
111
- logical: str | None = None
111
+ neatId: str | URIRef | None = None
112
+ logical: str | URIRef | None = None
112
113
 
113
114
  @classmethod
114
115
  def _get_verified_cls(cls) -> type[DMSProperty]:
@@ -139,6 +140,7 @@ class DMSInputContainer(InputComponent[DMSContainer]):
139
140
  name: str | None = None
140
141
  description: str | None = None
141
142
  constraint: str | None = None
143
+ neatId: str | URIRef | None = None
142
144
  used_for: Literal["node", "edge", "all"] | None = None
143
145
 
144
146
  @classmethod
@@ -183,7 +185,8 @@ class DMSInputView(InputComponent[DMSView]):
183
185
  implements: str | None = None
184
186
  filter_: Literal["hasData", "nodeType", "rawFilter"] | str | None = None
185
187
  in_model: bool = True
186
- logical: str | None = None
188
+ neatId: str | URIRef | None = None
189
+ logical: str | URIRef | None = None
187
190
 
188
191
  @classmethod
189
192
  def _get_verified_cls(cls) -> type[DMSView]:
@@ -231,6 +234,7 @@ class DMSInputNode(InputComponent[DMSNode]):
231
234
  usage: Literal["type", "collocation"]
232
235
  name: str | None = None
233
236
  description: str | None = None
237
+ neatId: str | URIRef | None = None
234
238
 
235
239
  @classmethod
236
240
  def _get_verified_cls(cls) -> type[DMSNode]:
@@ -252,6 +256,7 @@ class DMSInputEnum(InputComponent[DMSEnum]):
252
256
  value: str
253
257
  name: str | None = None
254
258
  description: str | None = None
259
+ neatId: str | URIRef | None = None
255
260
 
256
261
  @classmethod
257
262
  def _get_verified_cls(cls) -> type[DMSEnum]: