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
|
@@ -14,19 +14,21 @@ from cognite.client.data_classes.data_modeling.ids import InstanceId
|
|
|
14
14
|
from cognite.client.data_classes.data_modeling.views import SingleEdgeConnection
|
|
15
15
|
from cognite.client.exceptions import CogniteAPIError
|
|
16
16
|
from pydantic import BaseModel, ValidationInfo, create_model, field_validator
|
|
17
|
-
from rdflib import RDF
|
|
17
|
+
from rdflib import RDF, URIRef
|
|
18
18
|
|
|
19
19
|
from cognite.neat._graph._tracking import LogTracker, Tracker
|
|
20
20
|
from cognite.neat._issues import IssueList, NeatIssue, NeatIssueList
|
|
21
21
|
from cognite.neat._issues.errors import (
|
|
22
|
-
|
|
22
|
+
ResourceConversionError,
|
|
23
23
|
ResourceCreationError,
|
|
24
24
|
ResourceDuplicatedError,
|
|
25
25
|
ResourceRetrievalError,
|
|
26
26
|
)
|
|
27
27
|
from cognite.neat._issues.warnings import PropertyTypeNotSupportedWarning
|
|
28
|
+
from cognite.neat._rules.analysis._dms import DMSAnalysis
|
|
28
29
|
from cognite.neat._rules.models import DMSRules
|
|
29
30
|
from cognite.neat._rules.models.data_types import _DATA_TYPE_BY_DMS_TYPE, Json
|
|
31
|
+
from cognite.neat._rules.models.entities._single_value import ViewEntity
|
|
30
32
|
from cognite.neat._shared import InstanceType
|
|
31
33
|
from cognite.neat._store import NeatGraphStore
|
|
32
34
|
from cognite.neat._utils.auxiliary import create_sha256_hash
|
|
@@ -52,16 +54,18 @@ class DMSLoader(CDFLoader[dm.InstanceApply]):
|
|
|
52
54
|
graph_store: NeatGraphStore,
|
|
53
55
|
data_model: dm.DataModel[dm.View] | None,
|
|
54
56
|
instance_space: str,
|
|
55
|
-
|
|
57
|
+
class_neat_id_by_view_id: dict[ViewId, URIRef] | None = None,
|
|
56
58
|
create_issues: Sequence[NeatIssue] | None = None,
|
|
57
59
|
tracker: type[Tracker] | None = None,
|
|
60
|
+
rules: DMSRules | None = None,
|
|
58
61
|
):
|
|
59
62
|
super().__init__(graph_store)
|
|
60
63
|
self.data_model = data_model
|
|
61
64
|
self.instance_space = instance_space
|
|
62
|
-
self.
|
|
65
|
+
self.class_neat_id_by_view_id = class_neat_id_by_view_id or {}
|
|
63
66
|
self._issues = IssueList(create_issues or [])
|
|
64
67
|
self._tracker: type[Tracker] = tracker or LogTracker
|
|
68
|
+
self.rules = rules
|
|
65
69
|
|
|
66
70
|
@classmethod
|
|
67
71
|
def from_data_model_id(
|
|
@@ -88,14 +92,24 @@ class DMSLoader(CDFLoader[dm.InstanceApply]):
|
|
|
88
92
|
data_model = rules.as_schema().as_read_model()
|
|
89
93
|
except Exception as e:
|
|
90
94
|
issues.append(
|
|
91
|
-
|
|
95
|
+
ResourceConversionError(
|
|
92
96
|
identifier=rules.metadata.as_identifier(),
|
|
93
97
|
resource_type="DMS Rules",
|
|
94
98
|
target_format="read DMS model",
|
|
95
99
|
reason=str(e),
|
|
96
100
|
)
|
|
97
101
|
)
|
|
98
|
-
|
|
102
|
+
|
|
103
|
+
class_neat_id_by_view_id = {view.view.as_id(): view.logical for view in rules.views if view.logical}
|
|
104
|
+
|
|
105
|
+
return cls(
|
|
106
|
+
graph_store,
|
|
107
|
+
data_model,
|
|
108
|
+
instance_space,
|
|
109
|
+
class_neat_id_by_view_id,
|
|
110
|
+
issues,
|
|
111
|
+
rules=rules,
|
|
112
|
+
)
|
|
99
113
|
|
|
100
114
|
def _load(self, stop_on_exception: bool = False) -> Iterable[dm.InstanceApply | NeatIssue]:
|
|
101
115
|
if self._issues.has_errors and stop_on_exception:
|
|
@@ -106,6 +120,13 @@ class DMSLoader(CDFLoader[dm.InstanceApply]):
|
|
|
106
120
|
if not self.data_model:
|
|
107
121
|
# There should already be an error in this case.
|
|
108
122
|
return
|
|
123
|
+
|
|
124
|
+
views_with_linked_properties = (
|
|
125
|
+
DMSAnalysis(self.rules).views_with_properties_linked_to_classes(consider_inheritance=True)
|
|
126
|
+
if self.rules and self.rules.metadata.logical
|
|
127
|
+
else None
|
|
128
|
+
)
|
|
129
|
+
|
|
109
130
|
view_ids = [repr(v.as_id()) for v in self.data_model.views]
|
|
110
131
|
tracker = self._tracker(type(self).__name__, view_ids, "views")
|
|
111
132
|
for view in self.data_model.views:
|
|
@@ -114,17 +135,35 @@ class DMSLoader(CDFLoader[dm.InstanceApply]):
|
|
|
114
135
|
pydantic_cls, edge_by_type, issues = self._create_validation_classes(view) # type: ignore[var-annotated]
|
|
115
136
|
yield from issues
|
|
116
137
|
tracker.issue(issues)
|
|
117
|
-
class_name = self.class_by_view_id.get(view.as_id(), view.external_id)
|
|
118
138
|
|
|
119
|
-
|
|
139
|
+
# this assumes no changes in the suffix of view and class
|
|
140
|
+
|
|
141
|
+
if views_with_linked_properties:
|
|
142
|
+
# we need graceful exit if the view is not in the view_property_pairs
|
|
143
|
+
property_link_pairs = views_with_linked_properties.get(ViewEntity.from_id(view_id))
|
|
144
|
+
|
|
145
|
+
if class_neat_id := self.class_neat_id_by_view_id.get(view_id):
|
|
146
|
+
reader = self.graph_store._read_via_rules_linkage(class_neat_id, property_link_pairs)
|
|
147
|
+
else:
|
|
148
|
+
error_view = ResourceRetrievalError(view_id, "view", "View not linked to class")
|
|
149
|
+
tracker.issue(error_view)
|
|
150
|
+
if stop_on_exception:
|
|
151
|
+
raise error_view
|
|
152
|
+
yield error_view
|
|
153
|
+
|
|
154
|
+
else:
|
|
155
|
+
reader = self.graph_store.read(view.external_id)
|
|
156
|
+
|
|
157
|
+
for identifier, properties in reader:
|
|
120
158
|
try:
|
|
159
|
+
print(view_id)
|
|
121
160
|
yield self._create_node(identifier, properties, pydantic_cls, view_id)
|
|
122
161
|
except ValueError as e:
|
|
123
|
-
|
|
124
|
-
tracker.issue(
|
|
162
|
+
error_node = ResourceCreationError(identifier, "node", error=str(e))
|
|
163
|
+
tracker.issue(error_node)
|
|
125
164
|
if stop_on_exception:
|
|
126
|
-
raise
|
|
127
|
-
yield
|
|
165
|
+
raise error_node from e
|
|
166
|
+
yield error_node
|
|
128
167
|
yield from self._create_edges(identifier, properties, edge_by_type, tracker)
|
|
129
168
|
tracker.finish(repr(view_id))
|
|
130
169
|
|
|
@@ -244,7 +283,7 @@ class DMSLoader(CDFLoader[dm.InstanceApply]):
|
|
|
244
283
|
return dm.NodeApply(
|
|
245
284
|
space=self.instance_space,
|
|
246
285
|
external_id=identifier,
|
|
247
|
-
type=dm.DirectRelationReference(view_id.space,
|
|
286
|
+
type=(dm.DirectRelationReference(view_id.space, view_id.external_id) if type_ is not None else None),
|
|
248
287
|
sources=[dm.NodeOrEdgeData(source=view_id, properties=dict(created.model_dump().items()))],
|
|
249
288
|
)
|
|
250
289
|
|
|
@@ -5,7 +5,7 @@ from ._classic_cdf import (
|
|
|
5
5
|
AssetRelationshipConnector,
|
|
6
6
|
AssetSequenceConnector,
|
|
7
7
|
AssetTimeSeriesConnector,
|
|
8
|
-
|
|
8
|
+
RelationshipAsEdgeTransformer,
|
|
9
9
|
)
|
|
10
10
|
from ._rdfpath import AddSelfReferenceProperty, MakeConnectionOnExactMatch
|
|
11
11
|
from ._value_type import SplitMultiValueProperty
|
|
@@ -19,7 +19,7 @@ __all__ = [
|
|
|
19
19
|
"AssetRelationshipConnector",
|
|
20
20
|
"AddSelfReferenceProperty",
|
|
21
21
|
"SplitMultiValueProperty",
|
|
22
|
-
"
|
|
22
|
+
"RelationshipAsEdgeTransformer",
|
|
23
23
|
"MakeConnectionOnExactMatch",
|
|
24
24
|
]
|
|
25
25
|
|
|
@@ -32,6 +32,6 @@ Transformers = (
|
|
|
32
32
|
| AssetRelationshipConnector
|
|
33
33
|
| AddSelfReferenceProperty
|
|
34
34
|
| SplitMultiValueProperty
|
|
35
|
-
|
|
|
35
|
+
| RelationshipAsEdgeTransformer
|
|
36
36
|
| MakeConnectionOnExactMatch
|
|
37
37
|
)
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import textwrap
|
|
2
2
|
import warnings
|
|
3
3
|
from abc import ABC
|
|
4
|
+
from collections.abc import Callable, Iterable
|
|
5
|
+
from functools import lru_cache
|
|
4
6
|
from typing import cast
|
|
5
7
|
|
|
6
8
|
from rdflib import RDF, Graph, Literal, Namespace, URIRef
|
|
7
|
-
from rdflib.query import ResultRow
|
|
8
9
|
|
|
9
10
|
from cognite.neat._constants import CLASSIC_CDF_NAMESPACE, DEFAULT_NAMESPACE
|
|
10
11
|
from cognite.neat._graph import extractors
|
|
11
12
|
from cognite.neat._issues.warnings import ResourceNotFoundWarning
|
|
12
|
-
from cognite.neat._utils.
|
|
13
|
+
from cognite.neat._utils.collection_ import iterate_progress_bar
|
|
14
|
+
from cognite.neat._utils.rdf_ import (
|
|
15
|
+
Triple,
|
|
16
|
+
add_triples_in_batch,
|
|
17
|
+
remove_instance_ids_in_batch,
|
|
18
|
+
remove_namespace_from_uri,
|
|
19
|
+
)
|
|
13
20
|
|
|
14
21
|
from ._base import BaseTransformer
|
|
15
22
|
|
|
@@ -235,39 +242,48 @@ class AssetRelationshipConnector(BaseTransformer):
|
|
|
235
242
|
graph.remove((relationship_id, self.relationship_target_xid_prop, None))
|
|
236
243
|
|
|
237
244
|
|
|
238
|
-
class
|
|
239
|
-
"""
|
|
245
|
+
class RelationshipAsEdgeTransformer(BaseTransformer):
|
|
246
|
+
"""Converts relationships into edges in the graph.
|
|
240
247
|
|
|
241
|
-
This transformer
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
properties are replaced by a schema that contains the properties as attributes.
|
|
248
|
+
This transformer converts relationships into edges in the graph. This is useful as the
|
|
249
|
+
edges will be picked up as part of the schema connected to Assets, Events, Files, Sequenses,
|
|
250
|
+
and TimeSeries in the InferenceImporter.
|
|
245
251
|
|
|
246
252
|
Args:
|
|
247
|
-
|
|
248
|
-
to
|
|
253
|
+
min_relationship_types: The minimum number of relationship types that must exists to convert those
|
|
254
|
+
relationships to edges. For example, if there is only 5 relationships between Assets and TimeSeries,
|
|
255
|
+
and limit is 10, those relationships will not be converted to edges.
|
|
256
|
+
limit_per_type: The number of conversions to perform per relationship type. For example, if there are 10
|
|
257
|
+
relationships between Assets and TimeSeries, and limit_per_type is 1, only 1 of those relationships
|
|
258
|
+
will be converted to an edge. If None, all relationships will be converted.
|
|
249
259
|
|
|
250
260
|
"""
|
|
251
261
|
|
|
252
|
-
def __init__(
|
|
253
|
-
self
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
min_relationship_types: int = 1,
|
|
265
|
+
limit_per_type: int | None = None,
|
|
266
|
+
namespace: Namespace = CLASSIC_CDF_NAMESPACE,
|
|
267
|
+
) -> None:
|
|
268
|
+
self._min_relationship_types = min_relationship_types
|
|
269
|
+
self._limit_per_type = limit_per_type
|
|
254
270
|
self._namespace = namespace
|
|
255
271
|
|
|
256
272
|
_NOT_PROPERTIES: frozenset[str] = frozenset(
|
|
257
|
-
{"
|
|
273
|
+
{"sourceExternalId", "targetExternalId", "externalId", "sourceType", "targetType"}
|
|
258
274
|
)
|
|
259
275
|
_RELATIONSHIP_NODE_TYPES: tuple[str, ...] = tuple(["Asset", "Event", "File", "Sequence", "TimeSeries"])
|
|
260
|
-
description = "
|
|
276
|
+
description = "Converts relationships to edge"
|
|
261
277
|
_use_only_once: bool = True
|
|
262
|
-
_need_changes = frozenset({
|
|
278
|
+
_need_changes = frozenset({extractors.RelationshipsExtractor.__name__})
|
|
263
279
|
|
|
264
280
|
_count_by_source_target = """PREFIX classic: <{namespace}>
|
|
265
281
|
|
|
266
282
|
SELECT (COUNT(?instance) AS ?instanceCount)
|
|
267
283
|
WHERE {{
|
|
268
284
|
?instance a classic:Relationship .
|
|
269
|
-
?instance classic:
|
|
270
|
-
?instance classic:
|
|
285
|
+
?instance classic:sourceType classic:{source_type} .
|
|
286
|
+
?instance classic:targetType classic:{target_type} .
|
|
271
287
|
}}"""
|
|
272
288
|
|
|
273
289
|
_instances = """PREFIX classic: <{namespace}>
|
|
@@ -275,58 +291,110 @@ WHERE {{
|
|
|
275
291
|
SELECT ?instance
|
|
276
292
|
WHERE {{
|
|
277
293
|
?instance a classic:Relationship .
|
|
278
|
-
?instance classic:
|
|
279
|
-
?instance classic:
|
|
294
|
+
?instance classic:sourceType classic:{source_type} .
|
|
295
|
+
?instance classic:targetType classic:{target_type} .
|
|
280
296
|
}}"""
|
|
281
297
|
_lookup_entity_query = """PREFIX classic: <{namespace}>
|
|
282
298
|
|
|
283
299
|
SELECT ?entity
|
|
284
300
|
WHERE {{
|
|
285
301
|
?entity a classic:{entity_type} .
|
|
286
|
-
?entity classic:
|
|
302
|
+
?entity classic:externalId "{external_id}" .
|
|
287
303
|
}}"""
|
|
288
304
|
|
|
305
|
+
@staticmethod
|
|
306
|
+
def create_lookup_entity_with_external_id(graph: Graph, namespace: Namespace) -> Callable[[str, str], URIRef]:
|
|
307
|
+
@lru_cache(maxsize=10_000)
|
|
308
|
+
def lookup_entity_with_external_id(entity_type: str, external_id: str) -> URIRef:
|
|
309
|
+
query = RelationshipAsEdgeTransformer._lookup_entity_query.format(
|
|
310
|
+
namespace=namespace, entity_type=entity_type, external_id=external_id
|
|
311
|
+
)
|
|
312
|
+
result = list(graph.query(query))
|
|
313
|
+
if len(result) == 1:
|
|
314
|
+
return cast(URIRef, result[0][0]) # type: ignore[index]
|
|
315
|
+
raise ValueError(f"Could not find entity with external_id {external_id} and type {entity_type}")
|
|
316
|
+
|
|
317
|
+
return lookup_entity_with_external_id
|
|
318
|
+
|
|
289
319
|
def transform(self, graph: Graph) -> None:
|
|
320
|
+
lookup_entity_with_external_id = self.create_lookup_entity_with_external_id(graph, self._namespace)
|
|
290
321
|
for source_type in self._RELATIONSHIP_NODE_TYPES:
|
|
291
322
|
for target_type in self._RELATIONSHIP_NODE_TYPES:
|
|
292
323
|
query = self._count_by_source_target.format(
|
|
293
324
|
namespace=self._namespace, source_type=source_type, target_type=target_type
|
|
294
325
|
)
|
|
295
|
-
for
|
|
296
|
-
|
|
326
|
+
for instance_count_res in graph.query(query):
|
|
327
|
+
instance_count = int(instance_count_res[0]) # type: ignore[index, arg-type]
|
|
328
|
+
if instance_count < self._min_relationship_types:
|
|
297
329
|
continue
|
|
298
|
-
|
|
299
|
-
|
|
330
|
+
edge_triples = self._edge_triples(
|
|
331
|
+
graph, source_type, target_type, instance_count, lookup_entity_with_external_id
|
|
300
332
|
)
|
|
301
|
-
|
|
302
|
-
instance_id = cast(URIRef, result[0]) # type: ignore[index, misc]
|
|
303
|
-
self._convert_relationship_to_schema(graph, instance_id, source_type, target_type)
|
|
333
|
+
add_triples_in_batch(graph, edge_triples)
|
|
304
334
|
|
|
305
|
-
def
|
|
306
|
-
self,
|
|
307
|
-
|
|
308
|
-
|
|
335
|
+
def _edge_triples(
|
|
336
|
+
self,
|
|
337
|
+
graph: Graph,
|
|
338
|
+
source_type: str,
|
|
339
|
+
target_type: str,
|
|
340
|
+
instance_count: int,
|
|
341
|
+
lookup_entity_with_external_id: Callable[[str, str], URIRef],
|
|
342
|
+
) -> Iterable[Triple]:
|
|
343
|
+
query = self._instances.format(namespace=self._namespace, source_type=source_type, target_type=target_type)
|
|
344
|
+
total_instance_count = instance_count if self._limit_per_type is None else self._limit_per_type
|
|
345
|
+
|
|
346
|
+
converted_relationships: list[URIRef] = []
|
|
347
|
+
for no, result in enumerate(
|
|
348
|
+
iterate_progress_bar(graph.query(query), total=total_instance_count, description="Relationships to edges")
|
|
349
|
+
):
|
|
350
|
+
if self._limit_per_type is not None and no >= self._limit_per_type:
|
|
351
|
+
break
|
|
352
|
+
relationship_id = cast(URIRef, result[0]) # type: ignore[index, misc]
|
|
353
|
+
yield from self._relationship_as_edge(
|
|
354
|
+
graph, relationship_id, source_type, target_type, lookup_entity_with_external_id
|
|
355
|
+
)
|
|
356
|
+
converted_relationships.append(relationship_id)
|
|
357
|
+
|
|
358
|
+
if len(converted_relationships) >= 1_000:
|
|
359
|
+
remove_instance_ids_in_batch(graph, converted_relationships)
|
|
360
|
+
converted_relationships = []
|
|
361
|
+
|
|
362
|
+
remove_instance_ids_in_batch(graph, converted_relationships)
|
|
363
|
+
|
|
364
|
+
def _relationship_as_edge(
|
|
365
|
+
self,
|
|
366
|
+
graph: Graph,
|
|
367
|
+
relationship_id: URIRef,
|
|
368
|
+
source_type: str,
|
|
369
|
+
target_type: str,
|
|
370
|
+
lookup_entity_with_external_id: Callable[[str, str], URIRef],
|
|
371
|
+
) -> list[Triple]:
|
|
372
|
+
relationship_triples = cast(list[Triple], list(graph.query(f"DESCRIBE <{relationship_id}>")))
|
|
309
373
|
object_by_predicates = cast(
|
|
310
|
-
dict[str, URIRef | Literal], {remove_namespace_from_uri(row[1]): row[2] for row in
|
|
374
|
+
dict[str, URIRef | Literal], {remove_namespace_from_uri(row[1]): row[2] for row in relationship_triples}
|
|
311
375
|
)
|
|
312
|
-
source_external_id = cast(URIRef, object_by_predicates["
|
|
313
|
-
target_source_id = cast(URIRef, object_by_predicates["
|
|
376
|
+
source_external_id = cast(URIRef, object_by_predicates["sourceExternalId"])
|
|
377
|
+
target_source_id = cast(URIRef, object_by_predicates["targetExternalId"])
|
|
314
378
|
try:
|
|
315
|
-
source_id =
|
|
379
|
+
source_id = lookup_entity_with_external_id(source_type, source_external_id)
|
|
316
380
|
except ValueError:
|
|
317
|
-
warnings.warn(
|
|
318
|
-
|
|
381
|
+
warnings.warn(
|
|
382
|
+
ResourceNotFoundWarning(source_external_id, "class", str(relationship_id), "class"), stacklevel=2
|
|
383
|
+
)
|
|
384
|
+
return []
|
|
319
385
|
try:
|
|
320
|
-
target_id =
|
|
386
|
+
target_id = lookup_entity_with_external_id(target_type, target_source_id)
|
|
321
387
|
except ValueError:
|
|
322
|
-
warnings.warn(
|
|
323
|
-
|
|
324
|
-
|
|
388
|
+
warnings.warn(
|
|
389
|
+
ResourceNotFoundWarning(target_source_id, "class", str(relationship_id), "class"), stacklevel=2
|
|
390
|
+
)
|
|
391
|
+
return []
|
|
392
|
+
edge_id = str(object_by_predicates["externalId"])
|
|
325
393
|
# If there is properties on the relationship, we create a new intermediate node
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
394
|
+
edge_type = self._namespace[f"{source_type}To{target_type}Edge"]
|
|
395
|
+
return self._create_edge(
|
|
396
|
+
object_by_predicates, edge_id, source_id, target_id, self._predicate(target_type), edge_type
|
|
397
|
+
)
|
|
330
398
|
|
|
331
399
|
def _lookup_entity(self, graph: Graph, entity_type: str, external_id: str) -> URIRef:
|
|
332
400
|
query = self._lookup_entity_query.format(
|
|
@@ -337,27 +405,38 @@ WHERE {{
|
|
|
337
405
|
return cast(URIRef, result[0][0]) # type: ignore[index]
|
|
338
406
|
raise ValueError(f"Could not find entity with external_id {external_id} and type {entity_type}")
|
|
339
407
|
|
|
340
|
-
def
|
|
408
|
+
def _create_edge(
|
|
341
409
|
self,
|
|
342
|
-
graph: Graph,
|
|
343
410
|
objects_by_predicates: dict[str, URIRef | Literal],
|
|
344
411
|
external_id: str,
|
|
345
412
|
source_id: URIRef,
|
|
346
413
|
target_id: URIRef,
|
|
347
414
|
predicate: URIRef,
|
|
348
|
-
|
|
415
|
+
edge_type: URIRef,
|
|
416
|
+
) -> list[Triple]:
|
|
349
417
|
"""Creates a new intermediate node for the relationship with properties."""
|
|
350
|
-
# Create
|
|
351
|
-
|
|
352
|
-
|
|
418
|
+
# Create the entity with the properties
|
|
419
|
+
edge_triples: list[Triple] = []
|
|
420
|
+
edge_id = self._namespace[external_id]
|
|
421
|
+
|
|
422
|
+
edge_triples.append((edge_id, RDF.type, edge_type))
|
|
353
423
|
for prop_name, object_ in objects_by_predicates.items():
|
|
354
424
|
if prop_name in self._NOT_PROPERTIES:
|
|
355
425
|
continue
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
#
|
|
359
|
-
|
|
360
|
-
|
|
426
|
+
edge_triples.append((edge_id, self._namespace[prop_name], object_))
|
|
427
|
+
|
|
428
|
+
# Target and Source IDs will always be a combination of Asset, Sequence, Event, TimeSeries, and File.
|
|
429
|
+
# If we assume source ID is an asset and target ID is a time series, then
|
|
430
|
+
# before we had relationship pointing to both: timeseries <- relationship -> asset
|
|
431
|
+
# After, we want asset <-> Edge -> TimeSeries
|
|
432
|
+
# and the new edge will point to the asset and the timeseries through startNode and endNode
|
|
433
|
+
|
|
434
|
+
# Link the source to the new edge
|
|
435
|
+
edge_triples.append((source_id, predicate, edge_id))
|
|
436
|
+
# Link the edge to the source and target
|
|
437
|
+
edge_triples.append((edge_id, self._namespace["startNode"], source_id))
|
|
438
|
+
edge_triples.append((edge_id, self._namespace["endNode"], target_id))
|
|
439
|
+
return edge_triples
|
|
361
440
|
|
|
362
441
|
def _predicate(self, target_type: str) -> URIRef:
|
|
363
442
|
return self._namespace[f"relationship{target_type.capitalize()}"]
|
cognite/neat/_issues/_base.py
CHANGED
|
@@ -210,7 +210,7 @@ class NeatError(NeatIssue, Exception):
|
|
|
210
210
|
"""This is the base class for all exceptions (errors) used in Neat."""
|
|
211
211
|
|
|
212
212
|
@classmethod
|
|
213
|
-
def
|
|
213
|
+
def from_errors(cls, errors: "list[ErrorDetails | NeatError]", **kwargs) -> "list[NeatError]":
|
|
214
214
|
"""Convert a list of pydantic errors to a list of Error instances.
|
|
215
215
|
|
|
216
216
|
This is intended to be overridden in subclasses to handle specific error types.
|
|
@@ -219,24 +219,36 @@ class NeatError(NeatIssue, Exception):
|
|
|
219
219
|
read_info_by_sheet = kwargs.get("read_info_by_sheet")
|
|
220
220
|
|
|
221
221
|
for error in errors:
|
|
222
|
-
if
|
|
222
|
+
if (
|
|
223
|
+
isinstance(error, dict)
|
|
224
|
+
and error["type"] == "is_instance_of"
|
|
225
|
+
and error["loc"][1] == "is-instance[SheetList]"
|
|
226
|
+
):
|
|
223
227
|
# Skip the error for SheetList, as it is not relevant for the user. This is an
|
|
224
228
|
# internal class used to have helper methods for a lists as .to_pandas()
|
|
225
229
|
continue
|
|
226
|
-
|
|
227
|
-
if isinstance(
|
|
230
|
+
neat_error: NeatError | None = None
|
|
231
|
+
if isinstance(error, dict) and isinstance(ctx := error.get("ctx"), dict) and "error" in ctx:
|
|
232
|
+
neat_error = ctx["error"]
|
|
233
|
+
elif isinstance(error, NeatError | MultiValueError):
|
|
234
|
+
neat_error = error
|
|
235
|
+
|
|
236
|
+
if isinstance(neat_error, MultiValueError):
|
|
228
237
|
if read_info_by_sheet:
|
|
229
|
-
for caught_error in
|
|
238
|
+
for caught_error in neat_error.errors:
|
|
230
239
|
cls._adjust_row_numbers(caught_error, read_info_by_sheet) # type: ignore[arg-type]
|
|
231
|
-
all_errors.extend(
|
|
232
|
-
elif isinstance(
|
|
240
|
+
all_errors.extend(neat_error.errors) # type: ignore[arg-type]
|
|
241
|
+
elif isinstance(neat_error, NeatError):
|
|
233
242
|
if read_info_by_sheet:
|
|
234
|
-
cls._adjust_row_numbers(
|
|
235
|
-
all_errors.append(
|
|
236
|
-
elif len(error["loc"]) >= 4 and read_info_by_sheet:
|
|
243
|
+
cls._adjust_row_numbers(neat_error, read_info_by_sheet)
|
|
244
|
+
all_errors.append(neat_error)
|
|
245
|
+
elif isinstance(error, dict) and len(error["loc"]) >= 4 and read_info_by_sheet:
|
|
237
246
|
all_errors.append(RowError.from_pydantic_error(error, read_info_by_sheet))
|
|
238
|
-
|
|
247
|
+
elif isinstance(error, dict):
|
|
239
248
|
all_errors.append(DefaultPydanticError.from_pydantic_error(error))
|
|
249
|
+
else:
|
|
250
|
+
# This is unreachable. However, in case it turns out to be reachable, we want to know about it.
|
|
251
|
+
raise ValueError(f"Unsupported error type: {error}")
|
|
240
252
|
return all_errors
|
|
241
253
|
|
|
242
254
|
@staticmethod
|
|
@@ -511,13 +523,10 @@ def catch_issues(
|
|
|
511
523
|
try:
|
|
512
524
|
yield future_result
|
|
513
525
|
except ValidationError as e:
|
|
514
|
-
issues.extend(error_cls.
|
|
515
|
-
future_result._result = "failure"
|
|
516
|
-
except MultiValueError as e:
|
|
517
|
-
issues.extend(e.errors)
|
|
526
|
+
issues.extend(error_cls.from_errors(e.errors(), **(error_args or {}))) # type: ignore[arg-type]
|
|
518
527
|
future_result._result = "failure"
|
|
519
|
-
except NeatError as e:
|
|
520
|
-
issues.
|
|
528
|
+
except (NeatError, MultiValueError) as e:
|
|
529
|
+
issues.extend(error_cls.from_errors([e], **(error_args or {}))) # type: ignore[arg-type, list-item]
|
|
521
530
|
future_result._result = "failure"
|
|
522
531
|
else:
|
|
523
532
|
future_result._result = "success"
|
|
@@ -2,6 +2,7 @@ from cognite.neat._issues._base import DefaultPydanticError, NeatError, RowError
|
|
|
2
2
|
|
|
3
3
|
from ._external import (
|
|
4
4
|
AuthorizationError,
|
|
5
|
+
CDFMissingClientError,
|
|
5
6
|
FileMissingRequiredFieldError,
|
|
6
7
|
FileNotAFileError,
|
|
7
8
|
FileNotFoundNeatError,
|
|
@@ -20,7 +21,7 @@ from ._properties import (
|
|
|
20
21
|
)
|
|
21
22
|
from ._resources import (
|
|
22
23
|
ResourceChangedError,
|
|
23
|
-
|
|
24
|
+
ResourceConversionError,
|
|
24
25
|
ResourceCreationError,
|
|
25
26
|
ResourceDuplicatedError,
|
|
26
27
|
ResourceError,
|
|
@@ -58,7 +59,7 @@ __all__ = [
|
|
|
58
59
|
"ResourceError",
|
|
59
60
|
"ResourceNotDefinedError",
|
|
60
61
|
"ResourceMissingIdentifierError",
|
|
61
|
-
"
|
|
62
|
+
"ResourceConversionError",
|
|
62
63
|
"WorkflowConfigurationNotSetError",
|
|
63
64
|
"WorkFlowMissingDataError",
|
|
64
65
|
"WorkflowStepNotInitializedError",
|
|
@@ -70,6 +71,7 @@ __all__ = [
|
|
|
70
71
|
"RowError",
|
|
71
72
|
"NeatTypeError",
|
|
72
73
|
"ReversedConnectionNotFeasibleError",
|
|
74
|
+
"CDFMissingClientError",
|
|
73
75
|
]
|
|
74
76
|
|
|
75
77
|
_NEAT_ERRORS_BY_NAME = {error.__name__: error for error in _get_subclasses(NeatError, include_base=True)}
|
|
@@ -65,3 +65,10 @@ class FileNotAFileError(NeatError, FileNotFoundError):
|
|
|
65
65
|
|
|
66
66
|
fix = "Make sure to provide a valid file"
|
|
67
67
|
filepath: Path
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(unsafe_hash=True)
|
|
71
|
+
class CDFMissingClientError(NeatError, RuntimeError):
|
|
72
|
+
"""CDF client is required: {reason}"""
|
|
73
|
+
|
|
74
|
+
reason: str
|
|
@@ -34,14 +34,9 @@ class PropertyTypeNotSupportedError(PropertyError[T_Identifier]):
|
|
|
34
34
|
|
|
35
35
|
@dataclass(unsafe_hash=True)
|
|
36
36
|
class ReversedConnectionNotFeasibleError(PropertyError[T_Identifier]):
|
|
37
|
-
"""The {resource_type} {
|
|
38
|
-
since view {source_view_id} does not have direct connection {direct_connection} defined,
|
|
39
|
-
or {direct_connection} value type is not {target_view_id}
|
|
40
|
-
"""
|
|
37
|
+
"""The {resource_type} {identifier}.{property_name} cannot be created: {reason}"""
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
source_view_id: str
|
|
44
|
-
direct_connection: str
|
|
39
|
+
reason: str
|
|
45
40
|
|
|
46
41
|
|
|
47
42
|
# This is a generic error that should be used sparingly
|
|
@@ -64,7 +64,7 @@ class ResourceNotDefinedError(ResourceError[T_Identifier]):
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
@dataclass(unsafe_hash=True)
|
|
67
|
-
class
|
|
67
|
+
class ResourceConversionError(ResourceError, ValueError):
|
|
68
68
|
"""Failed to convert the {resource_type} {identifier} to {target_format}: {reason}"""
|
|
69
69
|
|
|
70
70
|
fix = "Check the error message and correct the rules."
|
|
@@ -6,7 +6,8 @@ from cognite.neat._issues._base import DefaultWarning, NeatWarning, _get_subclas
|
|
|
6
6
|
|
|
7
7
|
from . import user_modeling
|
|
8
8
|
from ._external import (
|
|
9
|
-
|
|
9
|
+
CDFAuthWarning,
|
|
10
|
+
CDFMaxIterationsWarning,
|
|
10
11
|
FileItemNotSupportedWarning,
|
|
11
12
|
FileMissingRequiredFieldWarning,
|
|
12
13
|
FileReadWarning,
|
|
@@ -35,6 +36,7 @@ from ._properties import (
|
|
|
35
36
|
from ._resources import (
|
|
36
37
|
ResourceNeatWarning,
|
|
37
38
|
ResourceNotFoundWarning,
|
|
39
|
+
ResourceRegexViolationWarning,
|
|
38
40
|
ResourceRetrievalWarning,
|
|
39
41
|
ResourcesDuplicatedWarning,
|
|
40
42
|
ResourceTypeNotSupportedWarning,
|
|
@@ -63,14 +65,16 @@ __all__ = [
|
|
|
63
65
|
"ResourceNotFoundWarning",
|
|
64
66
|
"ResourceTypeNotSupportedWarning",
|
|
65
67
|
"ResourceRetrievalWarning",
|
|
68
|
+
"ResourceRegexViolationWarning",
|
|
66
69
|
"PrincipleOneModelOneSpaceWarning",
|
|
67
70
|
"PrincipleMatchingSpaceAndVersionWarning",
|
|
68
71
|
"PrincipleSolutionBuildsOnEnterpriseWarning",
|
|
69
72
|
"NotSupportedViewContainerLimitWarning",
|
|
70
73
|
"NotSupportedHasDataFilterLimitWarning",
|
|
71
74
|
"UndefinedViewWarning",
|
|
72
|
-
"
|
|
75
|
+
"CDFAuthWarning",
|
|
73
76
|
"user_modeling",
|
|
77
|
+
"CDFMaxIterationsWarning",
|
|
74
78
|
]
|
|
75
79
|
|
|
76
80
|
_NEAT_WARNINGS_BY_NAME = {warning.__name__: warning for warning in _get_subclasses(NeatWarning, include_base=True)}
|
|
@@ -41,8 +41,16 @@ class FileItemNotSupportedWarning(NeatWarning):
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
@dataclass(unsafe_hash=True)
|
|
44
|
-
class
|
|
44
|
+
class CDFAuthWarning(NeatWarning):
|
|
45
45
|
"""Failed to {action} due to {reason}"""
|
|
46
46
|
|
|
47
47
|
action: str
|
|
48
48
|
reason: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(unsafe_hash=True)
|
|
52
|
+
class CDFMaxIterationsWarning(NeatWarning):
|
|
53
|
+
"""The maximum number of iterations ({max_iterations}) has been reached. {message}"""
|
|
54
|
+
|
|
55
|
+
message: str
|
|
56
|
+
max_iterations: int
|