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.
- cognite/neat/_client/_api/data_modeling_loaders.py +390 -116
- cognite/neat/_client/_api/schema.py +63 -2
- cognite/neat/_client/data_classes/data_modeling.py +4 -0
- cognite/neat/_client/data_classes/schema.py +2 -348
- cognite/neat/_constants.py +27 -4
- cognite/neat/_graph/extractors/_base.py +7 -0
- cognite/neat/_graph/extractors/_classic_cdf/_classic.py +28 -18
- cognite/neat/_graph/loaders/_rdf2dms.py +52 -13
- cognite/neat/_graph/transformers/__init__.py +3 -3
- cognite/neat/_graph/transformers/_classic_cdf.py +135 -56
- cognite/neat/_issues/_base.py +26 -17
- cognite/neat/_issues/errors/__init__.py +4 -2
- cognite/neat/_issues/errors/_external.py +7 -0
- cognite/neat/_issues/errors/_properties.py +2 -7
- cognite/neat/_issues/errors/_resources.py +1 -1
- cognite/neat/_issues/warnings/__init__.py +6 -2
- cognite/neat/_issues/warnings/_external.py +9 -1
- cognite/neat/_issues/warnings/_resources.py +41 -2
- cognite/neat/_issues/warnings/user_modeling.py +4 -4
- cognite/neat/_rules/_constants.py +2 -6
- cognite/neat/_rules/analysis/_base.py +15 -5
- cognite/neat/_rules/analysis/_dms.py +20 -0
- cognite/neat/_rules/analysis/_information.py +22 -0
- cognite/neat/_rules/exporters/_base.py +3 -5
- cognite/neat/_rules/exporters/_rules2dms.py +190 -200
- cognite/neat/_rules/importers/__init__.py +1 -3
- cognite/neat/_rules/importers/_base.py +1 -1
- cognite/neat/_rules/importers/_dms2rules.py +3 -25
- cognite/neat/_rules/importers/_rdf/__init__.py +5 -0
- cognite/neat/_rules/importers/_rdf/_base.py +34 -11
- cognite/neat/_rules/importers/_rdf/_imf2rules.py +91 -0
- cognite/neat/_rules/importers/_rdf/_inference2rules.py +40 -7
- cognite/neat/_rules/importers/_rdf/_owl2rules.py +80 -0
- cognite/neat/_rules/importers/_rdf/_shared.py +138 -441
- cognite/neat/_rules/models/_base_rules.py +19 -0
- cognite/neat/_rules/models/_types.py +5 -0
- cognite/neat/_rules/models/dms/__init__.py +2 -0
- cognite/neat/_rules/models/dms/_exporter.py +247 -123
- cognite/neat/_rules/models/dms/_rules.py +7 -49
- cognite/neat/_rules/models/dms/_rules_input.py +8 -3
- cognite/neat/_rules/models/dms/_validation.py +421 -123
- cognite/neat/_rules/models/entities/_multi_value.py +3 -0
- cognite/neat/_rules/models/information/__init__.py +2 -0
- cognite/neat/_rules/models/information/_rules.py +17 -61
- cognite/neat/_rules/models/information/_rules_input.py +11 -2
- cognite/neat/_rules/models/information/_validation.py +107 -11
- cognite/neat/_rules/models/mapping/_classic2core.py +1 -1
- cognite/neat/_rules/models/mapping/_classic2core.yaml +8 -4
- cognite/neat/_rules/transformers/__init__.py +2 -1
- cognite/neat/_rules/transformers/_converters.py +163 -61
- cognite/neat/_rules/transformers/_mapping.py +132 -2
- cognite/neat/_rules/transformers/_pipelines.py +1 -1
- cognite/neat/_rules/transformers/_verification.py +29 -4
- cognite/neat/_session/_base.py +46 -60
- cognite/neat/_session/_mapping.py +105 -5
- cognite/neat/_session/_prepare.py +49 -14
- cognite/neat/_session/_read.py +50 -4
- cognite/neat/_session/_set.py +1 -0
- cognite/neat/_session/_to.py +38 -12
- cognite/neat/_session/_wizard.py +5 -0
- cognite/neat/_session/engine/_interface.py +3 -2
- cognite/neat/_session/exceptions.py +4 -0
- cognite/neat/_store/_base.py +79 -19
- cognite/neat/_utils/collection_.py +22 -0
- cognite/neat/_utils/rdf_.py +30 -4
- cognite/neat/_version.py +2 -2
- cognite/neat/_workflows/steps/lib/current/rules_exporter.py +3 -91
- cognite/neat/_workflows/steps/lib/current/rules_importer.py +2 -16
- cognite/neat/_workflows/steps/lib/current/rules_validator.py +3 -5
- {cognite_neat-0.99.0.dist-info → cognite_neat-0.100.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.99.0.dist-info → cognite_neat-0.100.0.dist-info}/RECORD +74 -82
- cognite/neat/_rules/importers/_rdf/_imf2rules/__init__.py +0 -3
- cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2classes.py +0 -86
- cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2metadata.py +0 -29
- cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2properties.py +0 -130
- cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2rules.py +0 -154
- cognite/neat/_rules/importers/_rdf/_owl2rules/__init__.py +0 -3
- cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2classes.py +0 -58
- cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2metadata.py +0 -65
- cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2properties.py +0 -59
- cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2rules.py +0 -39
- {cognite_neat-0.99.0.dist-info → cognite_neat-0.100.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.99.0.dist-info → cognite_neat-0.100.0.dist-info}/WHEEL +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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(
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
508
|
+
f"Debug Info, Invalid valueType direct: {prop.model_dump_json()}"
|
|
437
509
|
)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]:
|