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
@@ -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
- ResourceConvertionError,
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
- class_by_view_id: dict[ViewId, str] | None = None,
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.class_by_view_id = class_by_view_id or {}
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
- ResourceConvertionError(
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
- return cls(graph_store, data_model, instance_space, {}, issues)
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
- for identifier, properties in self.graph_store.read(class_name):
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
- error = ResourceCreationError(identifier, "node", error=str(e))
124
- tracker.issue(error)
162
+ error_node = ResourceCreationError(identifier, "node", error=str(e))
163
+ tracker.issue(error_node)
125
164
  if stop_on_exception:
126
- raise error from e
127
- yield error
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, type_) if type_ is not None else None,
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
- RelationshipToSchemaTransformer,
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
- "RelationshipToSchemaTransformer",
22
+ "RelationshipAsEdgeTransformer",
23
23
  "MakeConnectionOnExactMatch",
24
24
  ]
25
25
 
@@ -32,6 +32,6 @@ Transformers = (
32
32
  | AssetRelationshipConnector
33
33
  | AddSelfReferenceProperty
34
34
  | SplitMultiValueProperty
35
- | RelationshipToSchemaTransformer
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.rdf_ import Triple, add_triples_in_batch, remove_namespace_from_uri
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 RelationshipToSchemaTransformer(BaseTransformer):
239
- """Replaces relationships with a schema.
245
+ class RelationshipAsEdgeTransformer(BaseTransformer):
246
+ """Converts relationships into edges in the graph.
240
247
 
241
- This transformer analyzes the relationships in the graph and modifies them to be part of the schema
242
- for Assets, Events, Files, Sequences, and TimeSeries. Relationships without any properties
243
- are replaced by a simple relationship between the source and target nodes. Relationships with
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
- limit: The minimum number of relationships that need to be present for it
248
- to be converted into a schema. Default is 1.
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__(self, limit: int = 1, namespace: Namespace = CLASSIC_CDF_NAMESPACE) -> None:
253
- self._limit = limit
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
- {"source_external_id", "target_external_id", "external_id", "source_type", "target_type"}
273
+ {"sourceExternalId", "targetExternalId", "externalId", "sourceType", "targetType"}
258
274
  )
259
275
  _RELATIONSHIP_NODE_TYPES: tuple[str, ...] = tuple(["Asset", "Event", "File", "Sequence", "TimeSeries"])
260
- description = "Replaces relationships with a schema"
276
+ description = "Converts relationships to edge"
261
277
  _use_only_once: bool = True
262
- _need_changes = frozenset({str(extractors.RelationshipsExtractor.__name__)})
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:source_type classic:{source_type} .
270
- ?instance classic:target_type classic:{target_type} .
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:source_type classic:{source_type} .
279
- ?instance classic:target_type classic:{target_type} .
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:external_id "{external_id}" .
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 instance_count in graph.query(query):
296
- if int(instance_count[0]) < self._limit: # type: ignore[index, arg-type]
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
- query = self._instances.format(
299
- namespace=self._namespace, source_type=source_type, target_type=target_type
330
+ edge_triples = self._edge_triples(
331
+ graph, source_type, target_type, instance_count, lookup_entity_with_external_id
300
332
  )
301
- for result in graph.query(query):
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 _convert_relationship_to_schema(
306
- self, graph: Graph, instance_id: URIRef, source_type: str, target_type: str
307
- ) -> None:
308
- result = cast(list[ResultRow], list(graph.query(f"DESCRIBE <{instance_id}>")))
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 result}
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["source_external_id"])
313
- target_source_id = cast(URIRef, object_by_predicates["target_external_id"])
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 = self._lookup_entity(graph, source_type, source_external_id)
379
+ source_id = lookup_entity_with_external_id(source_type, source_external_id)
316
380
  except ValueError:
317
- warnings.warn(ResourceNotFoundWarning(source_external_id, "class", str(instance_id), "class"), stacklevel=2)
318
- return None
381
+ warnings.warn(
382
+ ResourceNotFoundWarning(source_external_id, "class", str(relationship_id), "class"), stacklevel=2
383
+ )
384
+ return []
319
385
  try:
320
- target_id = self._lookup_entity(graph, target_type, target_source_id)
386
+ target_id = lookup_entity_with_external_id(target_type, target_source_id)
321
387
  except ValueError:
322
- warnings.warn(ResourceNotFoundWarning(target_source_id, "class", str(instance_id), "class"), stacklevel=2)
323
- return None
324
- external_id = str(object_by_predicates["external_id"])
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
- self._create_node(graph, object_by_predicates, external_id, source_id, target_id, self._predicate(target_type))
327
-
328
- for triple in result:
329
- graph.remove(triple) # type: ignore[arg-type]
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 _create_node(
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
- ) -> None:
415
+ edge_type: URIRef,
416
+ ) -> list[Triple]:
349
417
  """Creates a new intermediate node for the relationship with properties."""
350
- # Create new node
351
- instance_id = self._namespace[external_id]
352
- graph.add((instance_id, RDF.type, self._namespace["Edge"]))
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
- graph.add((instance_id, self._namespace[prop_name], object_))
357
-
358
- # Connect the new node to the source and target nodes
359
- graph.add((source_id, predicate, instance_id))
360
- graph.add((instance_id, self._namespace["end_node"], target_id))
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()}"]
@@ -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 from_pydantic_errors(cls, errors: list[ErrorDetails], **kwargs) -> "list[NeatError]":
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 error["type"] == "is_instance_of" and error["loc"][1] == "is-instance[SheetList]":
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
- ctx = error.get("ctx")
227
- if isinstance(ctx, dict) and isinstance(multi_error := ctx.get("error"), MultiValueError):
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 multi_error.errors:
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(multi_error.errors) # type: ignore[arg-type]
232
- elif isinstance(ctx, dict) and isinstance(single_error := ctx.get("error"), NeatError):
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(single_error, read_info_by_sheet)
235
- all_errors.append(single_error)
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
- else:
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.from_pydantic_errors(e.errors(), **(error_args or {})))
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.append(e)
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
- ResourceConvertionError,
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
- "ResourceConvertionError",
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} {property_name} with identifier {identifier} of the view {target_view_id} cannot be made
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
- target_view_id: str
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 ResourceConvertionError(ResourceError, ValueError):
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
- AuthWarning,
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
- "AuthWarning",
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 AuthWarning(NeatWarning):
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