cognite-neat 0.98.0__py3-none-any.whl → 0.99.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 (72) hide show
  1. cognite/neat/_client/__init__.py +4 -0
  2. cognite/neat/_client/_api/data_modeling_loaders.py +512 -0
  3. cognite/neat/_client/_api/schema.py +50 -0
  4. cognite/neat/_client/_api_client.py +17 -0
  5. cognite/neat/_client/data_classes/__init__.py +0 -0
  6. cognite/neat/{_utils/cdf/data_classes.py → _client/data_classes/data_modeling.py} +8 -135
  7. cognite/neat/{_rules/models/dms/_schema.py → _client/data_classes/schema.py} +21 -281
  8. cognite/neat/_graph/_shared.py +14 -15
  9. cognite/neat/_graph/extractors/_classic_cdf/_assets.py +14 -154
  10. cognite/neat/_graph/extractors/_classic_cdf/_base.py +154 -7
  11. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +23 -12
  12. cognite/neat/_graph/extractors/_classic_cdf/_data_sets.py +17 -92
  13. cognite/neat/_graph/extractors/_classic_cdf/_events.py +13 -162
  14. cognite/neat/_graph/extractors/_classic_cdf/_files.py +15 -179
  15. cognite/neat/_graph/extractors/_classic_cdf/_labels.py +32 -100
  16. cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +27 -178
  17. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +14 -139
  18. cognite/neat/_graph/extractors/_classic_cdf/_timeseries.py +15 -173
  19. cognite/neat/_graph/extractors/_rdf_file.py +6 -7
  20. cognite/neat/_graph/queries/_base.py +17 -1
  21. cognite/neat/_graph/transformers/_classic_cdf.py +50 -134
  22. cognite/neat/_graph/transformers/_prune_graph.py +1 -1
  23. cognite/neat/_graph/transformers/_rdfpath.py +1 -1
  24. cognite/neat/_issues/warnings/__init__.py +6 -0
  25. cognite/neat/_issues/warnings/_external.py +8 -0
  26. cognite/neat/_issues/warnings/_properties.py +16 -0
  27. cognite/neat/_rules/_constants.py +7 -6
  28. cognite/neat/_rules/analysis/_base.py +8 -4
  29. cognite/neat/_rules/exporters/_base.py +3 -4
  30. cognite/neat/_rules/exporters/_rules2dms.py +29 -40
  31. cognite/neat/_rules/importers/_dms2rules.py +4 -5
  32. cognite/neat/_rules/importers/_rdf/_inference2rules.py +25 -33
  33. cognite/neat/_rules/models/__init__.py +1 -1
  34. cognite/neat/_rules/models/_base_rules.py +22 -12
  35. cognite/neat/_rules/models/dms/__init__.py +2 -2
  36. cognite/neat/_rules/models/dms/_exporter.py +15 -20
  37. cognite/neat/_rules/models/dms/_rules.py +48 -3
  38. cognite/neat/_rules/models/dms/_rules_input.py +52 -8
  39. cognite/neat/_rules/models/dms/_validation.py +10 -5
  40. cognite/neat/_rules/models/entities/_single_value.py +32 -4
  41. cognite/neat/_rules/models/information/_rules.py +0 -8
  42. cognite/neat/_rules/models/mapping/__init__.py +2 -3
  43. cognite/neat/_rules/models/mapping/_classic2core.py +36 -146
  44. cognite/neat/_rules/models/mapping/_classic2core.yaml +339 -0
  45. cognite/neat/_rules/transformers/__init__.py +2 -2
  46. cognite/neat/_rules/transformers/_converters.py +110 -11
  47. cognite/neat/_rules/transformers/_mapping.py +105 -30
  48. cognite/neat/_rules/transformers/_verification.py +5 -2
  49. cognite/neat/_session/_base.py +49 -8
  50. cognite/neat/_session/_drop.py +35 -0
  51. cognite/neat/_session/_inspect.py +17 -5
  52. cognite/neat/_session/_mapping.py +39 -0
  53. cognite/neat/_session/_prepare.py +218 -23
  54. cognite/neat/_session/_read.py +49 -12
  55. cognite/neat/_session/_to.py +3 -3
  56. cognite/neat/_store/_base.py +27 -24
  57. cognite/neat/_utils/rdf_.py +28 -1
  58. cognite/neat/_version.py +1 -1
  59. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +8 -3
  60. cognite/neat/_workflows/steps/lib/current/rules_importer.py +4 -1
  61. cognite/neat/_workflows/steps/lib/current/rules_validator.py +3 -2
  62. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/METADATA +3 -3
  63. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/RECORD +67 -64
  64. cognite/neat/_rules/models/mapping/_base.py +0 -131
  65. cognite/neat/_utils/cdf/loaders/__init__.py +0 -25
  66. cognite/neat/_utils/cdf/loaders/_base.py +0 -54
  67. cognite/neat/_utils/cdf/loaders/_data_modeling.py +0 -339
  68. cognite/neat/_utils/cdf/loaders/_ingestion.py +0 -167
  69. /cognite/neat/{_utils/cdf → _client/_api}/__init__.py +0 -0
  70. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/LICENSE +0 -0
  71. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/WHEEL +0 -0
  72. {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/entry_points.txt +0 -0
@@ -12,7 +12,7 @@ from cognite.neat._rules._constants import EntityTypes
12
12
  from cognite.neat._rules.models.entities import ClassEntity
13
13
  from cognite.neat._rules.models.information import InformationRules
14
14
  from cognite.neat._shared import InstanceType
15
- from cognite.neat._utils.rdf_ import remove_namespace_from_uri
15
+ from cognite.neat._utils.rdf_ import remove_instance_ids_in_batch, remove_namespace_from_uri
16
16
 
17
17
  from ._construct import build_construct_query
18
18
 
@@ -342,3 +342,19 @@ class Queries:
342
342
  self.graph.query(query.format(unknownType=str(UNKNOWN_TYPE))),
343
343
  ):
344
344
  yield cast(URIRef, source_type), cast(URIRef, property_), [URIRef(uri) for uri in value_types.split(",")]
345
+
346
+ def drop_types(self, type_: list[URIRef]) -> dict[URIRef, int]:
347
+ """Drop types from the graph store
348
+
349
+ Args:
350
+ type_: List of types to drop
351
+
352
+ Returns:
353
+ Dictionary of dropped types
354
+ """
355
+ dropped_types: dict[URIRef, int] = {}
356
+ for t in type_:
357
+ instance_ids = self.list_instances_ids_of_class(t)
358
+ dropped_types[t] = len(instance_ids)
359
+ remove_instance_ids_in_batch(self.graph, instance_ids)
360
+ return dropped_types
@@ -1,4 +1,6 @@
1
+ import textwrap
1
2
  import warnings
3
+ from abc import ABC
2
4
  from typing import cast
3
5
 
4
6
  from rdflib import RDF, Graph, Literal, Namespace, URIRef
@@ -7,7 +9,7 @@ from rdflib.query import ResultRow
7
9
  from cognite.neat._constants import CLASSIC_CDF_NAMESPACE, DEFAULT_NAMESPACE
8
10
  from cognite.neat._graph import extractors
9
11
  from cognite.neat._issues.warnings import ResourceNotFoundWarning
10
- from cognite.neat._utils.rdf_ import remove_namespace_from_uri
12
+ from cognite.neat._utils.rdf_ import Triple, add_triples_in_batch, remove_namespace_from_uri
11
13
 
12
14
  from ._base import BaseTransformer
13
15
 
@@ -32,8 +34,8 @@ class AddAssetDepth(BaseTransformer):
32
34
  depth_typing: dict[int, str] | None = None,
33
35
  ):
34
36
  self.asset_type = asset_type or DEFAULT_NAMESPACE.Asset
35
- self.root_prop = root_prop or DEFAULT_NAMESPACE.root
36
- self.parent_prop = parent_prop or DEFAULT_NAMESPACE.parent
37
+ self.root_prop = root_prop or DEFAULT_NAMESPACE.rootId
38
+ self.parent_prop = parent_prop or DEFAULT_NAMESPACE.parentId
37
39
  self.depth_typing = depth_typing
38
40
 
39
41
  def transform(self, graph: Graph) -> None:
@@ -75,7 +77,33 @@ class AddAssetDepth(BaseTransformer):
75
77
  return None
76
78
 
77
79
 
78
- class AssetTimeSeriesConnector(BaseTransformer):
80
+ class BaseAssetConnector(BaseTransformer, ABC):
81
+ _asset_type: URIRef = DEFAULT_NAMESPACE.Asset
82
+ _item_type: URIRef
83
+ _default_attribute: URIRef
84
+ _connection_type: URIRef
85
+
86
+ _select_item_ids = "SELECT DISTINCT ?item_id WHERE {{?item_id a <{item_type}>}}"
87
+ _select_connected_assets: str = textwrap.dedent("""SELECT ?asset_id WHERE {{
88
+ <{item_id}> <{attribute}> ?asset_id .
89
+ ?asset_id a <{asset_type}>}}""")
90
+
91
+ def __init__(self, attribute: URIRef | None = None) -> None:
92
+ self._attribute = attribute or self._default_attribute
93
+
94
+ def transform(self, graph: Graph) -> None:
95
+ for item_id, *_ in graph.query(self._select_item_ids.format(item_type=self._item_type)): # type: ignore[misc]
96
+ triples: list[Triple] = []
97
+ for asset_id, *_ in graph.query( # type: ignore[misc]
98
+ self._select_connected_assets.format(
99
+ item_id=item_id, attribute=self._attribute, asset_type=self._asset_type
100
+ )
101
+ ):
102
+ triples.append((asset_id, self._connection_type, item_id)) # type: ignore[arg-type]
103
+ add_triples_in_batch(graph, triples)
104
+
105
+
106
+ class AssetTimeSeriesConnector(BaseAssetConnector):
79
107
  description: str = "Connects assets to timeseries, thus forming bi-directional connection"
80
108
  _use_only_once: bool = True
81
109
  _need_changes = frozenset(
@@ -84,41 +112,12 @@ class AssetTimeSeriesConnector(BaseTransformer):
84
112
  str(extractors.TimeSeriesExtractor.__name__),
85
113
  }
86
114
  )
87
- _asset_template: str = """SELECT ?asset_id WHERE {{
88
- <{timeseries_id}> <{asset_prop}> ?asset_id .
89
- ?asset_id a <{asset_type}>}}"""
115
+ _item_type = DEFAULT_NAMESPACE.TimeSeries
116
+ _default_attribute = DEFAULT_NAMESPACE.assetId
117
+ _connection_type = DEFAULT_NAMESPACE.timeSeries
90
118
 
91
- def __init__(
92
- self,
93
- asset_type: URIRef | None = None,
94
- timeseries_type: URIRef | None = None,
95
- asset_prop: URIRef | None = None,
96
- ):
97
- self.asset_type = asset_type or DEFAULT_NAMESPACE.Asset
98
- self.timeseries_type = timeseries_type or DEFAULT_NAMESPACE.TimeSeries
99
- self.asset_prop = asset_prop or DEFAULT_NAMESPACE.asset
100
119
 
101
- def transform(self, graph: Graph) -> None:
102
- for ts_id_result in graph.query(
103
- f"SELECT DISTINCT ?timeseries_id WHERE {{?timeseries_id a <{self.timeseries_type}>}}"
104
- ):
105
- timeseries_id: URIRef = cast(tuple, ts_id_result)[0]
106
-
107
- if asset_id_res := list(
108
- graph.query(
109
- self._asset_template.format(
110
- timeseries_id=timeseries_id,
111
- asset_prop=self.asset_prop,
112
- asset_type=self.asset_type,
113
- )
114
- )
115
- ):
116
- # timeseries can be connected to only one asset in the graph
117
- asset_id = cast(list[tuple], asset_id_res)[0][0]
118
- graph.add((asset_id, DEFAULT_NAMESPACE.timeSeries, timeseries_id))
119
-
120
-
121
- class AssetSequenceConnector(BaseTransformer):
120
+ class AssetSequenceConnector(BaseAssetConnector):
122
121
  description: str = "Connects assets to sequences, thus forming bi-directional connection"
123
122
  _use_only_once: bool = True
124
123
  _need_changes = frozenset(
@@ -127,41 +126,12 @@ class AssetSequenceConnector(BaseTransformer):
127
126
  str(extractors.SequencesExtractor.__name__),
128
127
  }
129
128
  )
130
- _asset_template: str = """SELECT ?asset_id WHERE {{
131
- <{sequence_id}> <{asset_prop}> ?asset_id .
132
- ?asset_id a <{asset_type}>}}"""
133
-
134
- def __init__(
135
- self,
136
- asset_type: URIRef | None = None,
137
- sequence_type: URIRef | None = None,
138
- asset_prop: URIRef | None = None,
139
- ):
140
- self.asset_type = asset_type or DEFAULT_NAMESPACE.Asset
141
- self.sequence_type = sequence_type or DEFAULT_NAMESPACE.Sequence
142
- self.asset_prop = asset_prop or DEFAULT_NAMESPACE.asset
143
-
144
- def transform(self, graph: Graph) -> None:
145
- for sequency_id_result in graph.query(
146
- f"SELECT DISTINCT ?sequence_id WHERE {{?sequence_id a <{self.sequence_type}>}}"
147
- ):
148
- sequence_id: URIRef = cast(tuple, sequency_id_result)[0]
149
-
150
- if asset_id_res := list(
151
- graph.query(
152
- self._asset_template.format(
153
- sequence_id=sequence_id,
154
- asset_prop=self.asset_prop,
155
- asset_type=self.asset_type,
156
- )
157
- )
158
- ):
159
- # sequence can be connected to only one asset in the graph
160
- asset_id = cast(list[tuple], asset_id_res)[0][0]
161
- graph.add((asset_id, DEFAULT_NAMESPACE.sequence, sequence_id))
129
+ _item_type = DEFAULT_NAMESPACE.Sequence
130
+ _default_attribute = DEFAULT_NAMESPACE.assetId
131
+ _connection_type = DEFAULT_NAMESPACE.sequence
162
132
 
163
133
 
164
- class AssetFileConnector(BaseTransformer):
134
+ class AssetFileConnector(BaseAssetConnector):
165
135
  description: str = "Connects assets to files, thus forming bi-directional connection"
166
136
  _use_only_once: bool = True
167
137
  _need_changes = frozenset(
@@ -170,39 +140,12 @@ class AssetFileConnector(BaseTransformer):
170
140
  str(extractors.FilesExtractor.__name__),
171
141
  }
172
142
  )
173
- _asset_template: str = """SELECT ?asset_id WHERE {{
174
- <{file_id}> <{asset_prop}> ?asset_id .
175
- ?asset_id a <{asset_type}>}}"""
176
-
177
- def __init__(
178
- self,
179
- asset_type: URIRef | None = None,
180
- file_type: URIRef | None = None,
181
- asset_prop: URIRef | None = None,
182
- ):
183
- self.asset_type = asset_type or DEFAULT_NAMESPACE.Asset
184
- self.file_type = file_type or DEFAULT_NAMESPACE.File
185
- self.asset_prop = asset_prop or DEFAULT_NAMESPACE.asset
143
+ _item_type = DEFAULT_NAMESPACE.File
144
+ _default_attribute = DEFAULT_NAMESPACE.assetIds
145
+ _connection_type = DEFAULT_NAMESPACE.file
186
146
 
187
- def transform(self, graph: Graph) -> None:
188
- for sequency_id_result in graph.query(f"SELECT DISTINCT ?file_id WHERE {{?file_id a <{self.file_type}>}}"):
189
- file_id: URIRef = cast(tuple, sequency_id_result)[0]
190
-
191
- if assets_id_res := list(
192
- graph.query(
193
- self._asset_template.format(
194
- file_id=file_id,
195
- asset_prop=self.asset_prop,
196
- asset_type=self.asset_type,
197
- )
198
- )
199
- ):
200
- # files can be connected to multiple assets in the graph
201
- for (asset_id,) in cast(list[tuple], assets_id_res):
202
- graph.add((asset_id, DEFAULT_NAMESPACE.file, file_id))
203
147
 
204
-
205
- class AssetEventConnector(BaseTransformer):
148
+ class AssetEventConnector(BaseAssetConnector):
206
149
  description: str = "Connects assets to events, thus forming bi-directional connection"
207
150
  _use_only_once: bool = True
208
151
  _need_changes = frozenset(
@@ -211,36 +154,9 @@ class AssetEventConnector(BaseTransformer):
211
154
  str(extractors.EventsExtractor.__name__),
212
155
  }
213
156
  )
214
- _asset_template: str = """SELECT ?asset_id WHERE {{
215
- <{event_id}> <{asset_prop}> ?asset_id .
216
- ?asset_id a <{asset_type}>}}"""
217
-
218
- def __init__(
219
- self,
220
- asset_type: URIRef | None = None,
221
- event_type: URIRef | None = None,
222
- asset_prop: URIRef | None = None,
223
- ):
224
- self.asset_type = asset_type or DEFAULT_NAMESPACE.Asset
225
- self.event_type = event_type or DEFAULT_NAMESPACE.Event
226
- self.asset_prop = asset_prop or DEFAULT_NAMESPACE.asset
227
-
228
- def transform(self, graph: Graph) -> None:
229
- for event_id_result in graph.query(f"SELECT DISTINCT ?event_id WHERE {{?event_id a <{self.event_type}>}}"):
230
- event_id: URIRef = cast(tuple, event_id_result)[0]
231
-
232
- if assets_id_res := list(
233
- graph.query(
234
- self._asset_template.format(
235
- event_id=event_id,
236
- asset_prop=self.asset_prop,
237
- asset_type=self.asset_type,
238
- )
239
- )
240
- ):
241
- # files can be connected to multiple assets in the graph
242
- for (asset_id,) in cast(list[tuple], assets_id_res):
243
- graph.add((asset_id, DEFAULT_NAMESPACE.event, event_id))
157
+ _item_type = DEFAULT_NAMESPACE.Event
158
+ _default_attribute = DEFAULT_NAMESPACE.assetIds
159
+ _connection_type = DEFAULT_NAMESPACE.event
244
160
 
245
161
 
246
162
  class AssetRelationshipConnector(BaseTransformer):
@@ -271,9 +187,9 @@ class AssetRelationshipConnector(BaseTransformer):
271
187
  ):
272
188
  self.asset_type = asset_type or DEFAULT_NAMESPACE.Asset
273
189
  self.relationship_type = relationship_type or DEFAULT_NAMESPACE.Relationship
274
- self.relationship_source_xid_prop = relationship_source_xid_prop or DEFAULT_NAMESPACE.source_external_id
275
- self.relationship_target_xid_prop = relationship_target_xid_prop or DEFAULT_NAMESPACE.target_external_id
276
- self.asset_xid_property = asset_xid_property or DEFAULT_NAMESPACE.external_id
190
+ self.relationship_source_xid_prop = relationship_source_xid_prop or DEFAULT_NAMESPACE.sourceExternalId
191
+ self.relationship_target_xid_prop = relationship_target_xid_prop or DEFAULT_NAMESPACE.targetExternalId
192
+ self.asset_xid_property = asset_xid_property or DEFAULT_NAMESPACE.externalId
277
193
 
278
194
  def transform(self, graph: Graph) -> None:
279
195
  for relationship_id_result in graph.query(
@@ -123,4 +123,4 @@ class PruneDanglingNodes(BaseTransformer):
123
123
  for node in nodes_without_neighbours:
124
124
  # Remove node and its property triples in the graph
125
125
  if isinstance(node, ResultRow):
126
- graph.remove(triple=(node["subject"], None, None))
126
+ graph.remove((node["subject"], None, None))
@@ -49,7 +49,7 @@ class AddSelfReferenceProperty(BaseTransformer):
49
49
  )
50
50
 
51
51
  traversal = SingleProperty.from_string(
52
- class_=property_.class_.id,
52
+ class_=property_.view.id,
53
53
  property_=f"{self.rules.metadata.prefix}:{property_.property_}",
54
54
  )
55
55
 
@@ -6,6 +6,7 @@ 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
10
  FileItemNotSupportedWarning,
10
11
  FileMissingRequiredFieldWarning,
11
12
  FileReadWarning,
@@ -26,6 +27,8 @@ from ._models import (
26
27
  from ._properties import (
27
28
  PropertyDefinitionDuplicatedWarning,
28
29
  PropertyNotFoundWarning,
30
+ PropertyOverwritingWarning,
31
+ PropertySkippedWarning,
29
32
  PropertyTypeNotSupportedWarning,
30
33
  PropertyValueTypeUndefinedWarning,
31
34
  )
@@ -52,6 +55,8 @@ __all__ = [
52
55
  "PropertyTypeNotSupportedWarning",
53
56
  "PropertyNotFoundWarning",
54
57
  "PropertyValueTypeUndefinedWarning",
58
+ "PropertyOverwritingWarning",
59
+ "PropertySkippedWarning",
55
60
  "ResourceNeatWarning",
56
61
  "ResourcesDuplicatedWarning",
57
62
  "RegexViolationWarning",
@@ -64,6 +69,7 @@ __all__ = [
64
69
  "NotSupportedViewContainerLimitWarning",
65
70
  "NotSupportedHasDataFilterLimitWarning",
66
71
  "UndefinedViewWarning",
72
+ "AuthWarning",
67
73
  "user_modeling",
68
74
  ]
69
75
 
@@ -38,3 +38,11 @@ class FileItemNotSupportedWarning(NeatWarning):
38
38
 
39
39
  item: str
40
40
  filepath: Path
41
+
42
+
43
+ @dataclass(unsafe_hash=True)
44
+ class AuthWarning(NeatWarning):
45
+ """Failed to {action} due to {reason}"""
46
+
47
+ action: str
48
+ reason: str
@@ -54,3 +54,19 @@ class PropertyValueTypeUndefinedWarning(PropertyWarning[T_Identifier]):
54
54
 
55
55
  default_action: str
56
56
  recommended_action: str | None = None
57
+
58
+
59
+ @dataclass(unsafe_hash=True)
60
+ class PropertyOverwritingWarning(PropertyWarning[T_Identifier]):
61
+ """Overwriting the {overwriting} for {property_name} in the {resource_type}
62
+ with identifier {identifier}."""
63
+
64
+ overwriting: tuple[str, ...]
65
+
66
+
67
+ @dataclass(unsafe_hash=True)
68
+ class PropertySkippedWarning(PropertyWarning[T_Identifier]):
69
+ """The {resource_type} with identifier {identifier} has a property {property_name}
70
+ which is skipped. {reason}."""
71
+
72
+ reason: str
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  import sys
3
3
  from functools import cached_property
4
+ from typing import Literal
4
5
 
5
6
  if sys.version_info >= (3, 11):
6
7
  from enum import StrEnum
@@ -42,7 +43,7 @@ class EntityTypes(StrEnum):
42
43
  space = "space"
43
44
 
44
45
 
45
- def get_reserved_words() -> dict[str, list[str]]:
46
+ def get_reserved_words(key: Literal["class", "view", "property", "space"]) -> list[str]:
46
47
  return {
47
48
  "class": ["Class", "class"],
48
49
  "view": [
@@ -82,7 +83,7 @@ def get_reserved_words() -> dict[str, list[str]]:
82
83
  "extensions",
83
84
  ],
84
85
  "space": ["space", "cdf", "dms", "pg3", "shared", "system", "node", "edge"],
85
- }
86
+ }[key]
86
87
 
87
88
 
88
89
  ENTITY_PATTERN = re.compile(r"^(?P<prefix>.*?):?(?P<suffix>[^(:]*)(\((?P<content>.+)\))?$")
@@ -93,20 +94,20 @@ MORE_THAN_ONE_NONE_ALPHANUMERIC_REGEX = r"([_-]{2,})"
93
94
  PREFIX_COMPLIANCE_REGEX = r"^([a-zA-Z]+)([a-zA-Z0-9]*[_-]{0,1}[a-zA-Z0-9_-]*)([a-zA-Z0-9]*)$"
94
95
 
95
96
  SPACE_COMPLIANCE_REGEX = (
96
- rf"(?!^({'|'.join(get_reserved_words()['space'])})$)" r"(^[a-zA-Z][a-zA-Z0-9_-]{0,41}[a-zA-Z0-9]?$)"
97
+ rf"(?!^({'|'.join(get_reserved_words('space'))})$)" r"(^[a-zA-Z][a-zA-Z0-9_-]{0,41}[a-zA-Z0-9]?$)"
97
98
  )
98
99
 
99
100
 
100
101
  DATA_MODEL_COMPLIANCE_REGEX = r"^[a-zA-Z]([a-zA-Z0-9_]{0,253}[a-zA-Z0-9])?$"
101
102
 
102
103
  VIEW_ID_COMPLIANCE_REGEX = (
103
- rf"(?!^({'|'.join(get_reserved_words()['view'])})$)" r"(^[a-zA-Z][a-zA-Z0-9_]{0,253}[a-zA-Z0-9]?$)"
104
+ rf"(?!^({'|'.join(get_reserved_words('view'))})$)" r"(^[a-zA-Z][a-zA-Z0-9_]{0,253}[a-zA-Z0-9]?$)"
104
105
  )
105
106
  DMS_PROPERTY_ID_COMPLIANCE_REGEX = (
106
- rf"(?!^({'|'.join(get_reserved_words()['property'])})$)" r"(^[a-zA-Z][a-zA-Z0-9_]{0,253}[a-zA-Z0-9]?$)"
107
+ rf"(?!^({'|'.join(get_reserved_words('property'))})$)" r"(^[a-zA-Z][a-zA-Z0-9_]{0,253}[a-zA-Z0-9]?$)"
107
108
  )
108
109
  CLASS_ID_COMPLIANCE_REGEX = (
109
- rf"(?!^({'|'.join(get_reserved_words()['class'])})$)" r"(^[a-zA-Z][a-zA-Z0-9._-]{0,253}[a-zA-Z0-9]?$)"
110
+ rf"(?!^({'|'.join(get_reserved_words('class'))})$)" r"(^[a-zA-Z][a-zA-Z0-9._-]{0,253}[a-zA-Z0-9]?$)"
110
111
  )
111
112
 
112
113
  INFORMATION_PROPERTY_ID_COMPLIANCE_REGEX = (
@@ -109,14 +109,14 @@ class BaseAnalysis(ABC, Generic[T_Rules, T_Class, T_Property, T_ClassEntity, T_P
109
109
  raise NotImplementedError
110
110
 
111
111
  # Todo Lru cache this method.
112
- def class_parent_pairs(self) -> dict[T_ClassEntity, list[T_ClassEntity]]:
112
+ def class_parent_pairs(self, allow_different_space: bool = False) -> dict[T_ClassEntity, list[T_ClassEntity]]:
113
113
  """This only returns class - parent pairs only if parent is in the same data model"""
114
114
  class_subclass_pairs: dict[T_ClassEntity, list[T_ClassEntity]] = {}
115
115
  for cls_ in self._get_classes():
116
116
  entity = self._get_cls_entity(cls_)
117
117
  class_subclass_pairs[entity] = []
118
118
  for parent in self._get_cls_parents(cls_) or []:
119
- if parent.prefix == entity.prefix:
119
+ if parent.prefix == entity.prefix or allow_different_space:
120
120
  class_subclass_pairs[entity].append(parent)
121
121
  else:
122
122
  warnings.warn(
@@ -126,11 +126,15 @@ class BaseAnalysis(ABC, Generic[T_Rules, T_Class, T_Property, T_ClassEntity, T_P
126
126
 
127
127
  return class_subclass_pairs
128
128
 
129
- def classes_with_properties(self, consider_inheritance: bool = False) -> dict[T_ClassEntity, list[T_Property]]:
129
+ def classes_with_properties(
130
+ self, consider_inheritance: bool = False, allow_different_namespace: bool = False
131
+ ) -> dict[T_ClassEntity, list[T_Property]]:
130
132
  """Returns classes that have been defined in the data model.
131
133
 
132
134
  Args:
133
135
  consider_inheritance: Whether to consider inheritance or not. Defaults False
136
+ allow_different_namespace: When considering inheritance, whether to allow parents from
137
+ different namespaces or not. Defaults False
134
138
 
135
139
  Returns:
136
140
  Dictionary of classes with a list of properties defined for them
@@ -150,7 +154,7 @@ class BaseAnalysis(ABC, Generic[T_Rules, T_Class, T_Property, T_ClassEntity, T_P
150
154
  class_property_pairs[self._get_cls_entity(property_)].append(property_) # type: ignore
151
155
 
152
156
  if consider_inheritance:
153
- class_parent_pairs = self.class_parent_pairs()
157
+ class_parent_pairs = self.class_parent_pairs(allow_different_namespace)
154
158
  for class_ in class_parent_pairs:
155
159
  self._add_inherited_properties(class_, class_property_pairs, class_parent_pairs)
156
160
 
@@ -3,8 +3,7 @@ from collections.abc import Iterable
3
3
  from pathlib import Path
4
4
  from typing import Generic, TypeVar
5
5
 
6
- from cognite.client import CogniteClient
7
-
6
+ from cognite.neat._client import NeatClient
8
7
  from cognite.neat._rules._shared import T_VerifiedRules
9
8
  from cognite.neat._utils.auxiliary import class_html_doc
10
9
  from cognite.neat._utils.upload import UploadResult, UploadResultList
@@ -32,11 +31,11 @@ class BaseExporter(ABC, Generic[T_VerifiedRules, T_Export]):
32
31
  class CDFExporter(BaseExporter[T_VerifiedRules, T_Export]):
33
32
  @abstractmethod
34
33
  def export_to_cdf_iterable(
35
- self, rules: T_VerifiedRules, client: CogniteClient, dry_run: bool = False, fallback_one_by_one: bool = False
34
+ self, rules: T_VerifiedRules, client: NeatClient, dry_run: bool = False, fallback_one_by_one: bool = False
36
35
  ) -> Iterable[UploadResult]:
37
36
  raise NotImplementedError
38
37
 
39
38
  def export_to_cdf(
40
- self, rules: T_VerifiedRules, client: CogniteClient, dry_run: bool = False, fallback_one_by_one: bool = False
39
+ self, rules: T_VerifiedRules, client: NeatClient, dry_run: bool = False, fallback_one_by_one: bool = False
41
40
  ) -> UploadResultList:
42
41
  return UploadResultList(self.export_to_cdf_iterable(rules, client, dry_run, fallback_one_by_one))
@@ -3,7 +3,6 @@ from collections.abc import Collection, Hashable, Iterable, Sequence
3
3
  from pathlib import Path
4
4
  from typing import Literal, TypeAlias, cast
5
5
 
6
- from cognite.client import CogniteClient
7
6
  from cognite.client.data_classes._base import CogniteResource, CogniteResourceList
8
7
  from cognite.client.data_classes.data_modeling import (
9
8
  ContainerApplyList,
@@ -15,23 +14,14 @@ from cognite.client.data_classes.data_modeling import (
15
14
  )
16
15
  from cognite.client.exceptions import CogniteAPIError
17
16
 
17
+ from cognite.neat._client import DataModelingLoader, NeatClient
18
+ from cognite.neat._client.data_classes.schema import DMSSchema
18
19
  from cognite.neat._issues import IssueList
19
20
  from cognite.neat._issues.warnings import (
20
21
  PrincipleOneModelOneSpaceWarning,
21
22
  ResourceRetrievalWarning,
22
23
  )
23
- from cognite.neat._rules.models.dms import DMSRules, DMSSchema, PipelineSchema
24
- from cognite.neat._utils.cdf.loaders import (
25
- ContainerLoader,
26
- DataModelingLoader,
27
- DataModelLoader,
28
- RawDatabaseLoader,
29
- RawTableLoader,
30
- ResourceLoader,
31
- SpaceLoader,
32
- TransformationLoader,
33
- ViewLoader,
34
- )
24
+ from cognite.neat._rules.models.dms import DMSRules
35
25
  from cognite.neat._utils.upload import UploadResult
36
26
 
37
27
  from ._base import CDFExporter
@@ -119,14 +109,14 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
119
109
  return rules.as_schema(include_pipeline=self.export_pipeline, instance_space=self.instance_space)
120
110
 
121
111
  def delete_from_cdf(
122
- self, rules: DMSRules, client: CogniteClient, dry_run: bool = False, skip_space: bool = False
112
+ self, rules: DMSRules, client: NeatClient, dry_run: bool = False, skip_space: bool = False
123
113
  ) -> Iterable[UploadResult]:
124
- to_export = self._prepare_exporters(rules, client)
114
+ to_export = self._prepare_exporters(rules)
125
115
 
126
116
  # we need to reverse order in which we are picking up the items to delete
127
117
  # as they are sorted in the order of creation and we need to delete them in reverse order
128
118
  for items, loader in reversed(to_export):
129
- if skip_space and isinstance(loader, SpaceLoader):
119
+ if skip_space and isinstance(items, SpaceApplyList):
130
120
  continue
131
121
  item_ids = loader.get_ids(items)
132
122
  existing_items = loader.retrieve(item_ids)
@@ -166,9 +156,9 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
166
156
  )
167
157
 
168
158
  def export_to_cdf_iterable(
169
- self, rules: DMSRules, client: CogniteClient, dry_run: bool = False, fallback_one_by_one: bool = False
159
+ self, rules: DMSRules, client: NeatClient, dry_run: bool = False, fallback_one_by_one: bool = False
170
160
  ) -> Iterable[UploadResult]:
171
- to_export = self._prepare_exporters(rules, client)
161
+ to_export = self._prepare_exporters(rules)
172
162
 
173
163
  result_by_name = {}
174
164
  if self.existing_handling == "force":
@@ -176,17 +166,18 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
176
166
  result_by_name[delete_result.name] = delete_result
177
167
 
178
168
  redeploy_data_model = False
179
- for items, loader in to_export:
169
+ for items in to_export:
180
170
  # The conversion from DMS to GraphQL does not seem to be triggered even if the views
181
171
  # are changed. This is a workaround to force the conversion.
182
- is_redeploying = loader is DataModelingLoader and redeploy_data_model
172
+ is_redeploying = isinstance(items, DataModelApplyList) and redeploy_data_model
173
+ loader = client.loaders.get_loader(items)
183
174
 
184
175
  to_create, to_delete, to_update, unchanged = self._categorize_items_for_upload(
185
176
  loader, items, is_redeploying
186
177
  )
187
178
 
188
179
  issue_list = IssueList()
189
- warning_list = self._validate(loader, items)
180
+ warning_list = self._validate(loader, items, client)
190
181
  issue_list.extend(warning_list)
191
182
 
192
183
  created: set[Hashable] = set()
@@ -206,6 +197,8 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
206
197
  failed_changed.update(loader.get_id(item) for item in to_update)
207
198
  else:
208
199
  raise ValueError(f"Unsupported existing_handling {self.existing_handling}")
200
+ created.update(loader.get_id(item) for item in to_create)
201
+ deleted.update(loader.get_id(item) for item in to_delete)
209
202
  else:
210
203
  if to_delete:
211
204
  try:
@@ -226,7 +219,7 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
226
219
  else:
227
220
  deleted.update(loader.get_id(item) for item in to_delete)
228
221
 
229
- if isinstance(loader, DataModelingLoader):
222
+ if isinstance(items, DataModelApplyList):
230
223
  to_create = loader.sort_by_dependencies(to_create)
231
224
 
232
225
  try:
@@ -292,11 +285,11 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
292
285
  issues=issue_list,
293
286
  )
294
287
 
295
- if loader is ViewLoader and (created or changed):
288
+ if isinstance(items, ViewApplyList) and (created or changed):
296
289
  redeploy_data_model = True
297
290
 
298
291
  def _categorize_items_for_upload(
299
- self, loader: ResourceLoader, items: Sequence[CogniteResource], is_redeploying
292
+ self, loader: DataModelingLoader, items: Sequence[CogniteResource], is_redeploying
300
293
  ) -> tuple[list[CogniteResource], list[CogniteResource], list[CogniteResource], list[CogniteResource]]:
301
294
  item_ids = loader.get_ids(items)
302
295
  cdf_items = loader.retrieve(item_ids)
@@ -304,7 +297,7 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
304
297
  to_create, to_update, unchanged, to_delete = [], [], [], []
305
298
  for item in items:
306
299
  if (
307
- isinstance(loader, DataModelingLoader)
300
+ isinstance(items, DataModelApplyList)
308
301
  and self.include_space is not None
309
302
  and not loader.in_space(item, self.include_space)
310
303
  ):
@@ -322,28 +315,24 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
322
315
  to_update.append(item)
323
316
  return to_create, to_delete, to_update, unchanged
324
317
 
325
- def _prepare_exporters(self, rules, client) -> list[tuple[CogniteResourceList, ResourceLoader]]:
318
+ def _prepare_exporters(self, rules: DMSRules) -> list[CogniteResourceList]:
326
319
  schema = self.export(rules)
327
- to_export: list[tuple[CogniteResourceList, ResourceLoader]] = []
320
+ to_export: list[CogniteResourceList] = []
328
321
  if self.export_components.intersection({"all", "spaces"}):
329
- to_export.append((SpaceApplyList(schema.spaces.values()), SpaceLoader(client)))
322
+ to_export.append(SpaceApplyList(schema.spaces.values()))
330
323
  if self.export_components.intersection({"all", "containers"}):
331
- to_export.append((ContainerApplyList(schema.containers.values()), ContainerLoader(client)))
324
+ to_export.append(ContainerApplyList(schema.containers.values()))
332
325
  if self.export_components.intersection({"all", "views"}):
333
- to_export.append((ViewApplyList(schema.views.values()), ViewLoader(client, self.existing_handling)))
326
+ to_export.append(ViewApplyList(schema.views.values()))
334
327
  if self.export_components.intersection({"all", "data_models"}):
335
- to_export.append((DataModelApplyList([schema.data_model]), DataModelLoader(client)))
336
- if isinstance(schema, PipelineSchema):
337
- to_export.append((schema.databases, RawDatabaseLoader(client)))
338
- to_export.append((schema.raw_tables, RawTableLoader(client)))
339
- to_export.append((schema.transformations, TransformationLoader(client)))
328
+ to_export.append(DataModelApplyList([schema.data_model]))
340
329
  return to_export
341
330
 
342
- def _validate(self, loader: ResourceLoader, items: CogniteResourceList) -> IssueList:
331
+ def _validate(self, loader: DataModelingLoader, items: CogniteResourceList, client: NeatClient) -> IssueList:
343
332
  issue_list = IssueList()
344
- if isinstance(loader, DataModelLoader):
333
+ if isinstance(items, DataModelApplyList):
345
334
  models = cast(list[DataModelApply], items)
346
- if other_models := self._exist_other_data_models(loader, models):
335
+ if other_models := self._exist_other_data_models(client, models):
347
336
  warning = PrincipleOneModelOneSpaceWarning(
348
337
  f"There are multiple data models in the same space {models[0].space}. "
349
338
  f"Other data models in the space are {other_models}.",
@@ -355,13 +344,13 @@ class DMSExporter(CDFExporter[DMSRules, DMSSchema]):
355
344
  return issue_list
356
345
 
357
346
  @classmethod
358
- def _exist_other_data_models(cls, loader: DataModelLoader, models: list[DataModelApply]) -> list[DataModelId]:
347
+ def _exist_other_data_models(cls, client: NeatClient, models: list[DataModelApply]) -> list[DataModelId]:
359
348
  if not models:
360
349
  return []
361
350
  space = models[0].space
362
351
  external_id = models[0].external_id
363
352
  try:
364
- data_models = loader.client.data_modeling.data_models.list(space=space, limit=25, all_versions=False)
353
+ data_models = client.data_modeling.data_models.list(space=space, limit=25, all_versions=False)
365
354
  except CogniteAPIError as e:
366
355
  warnings.warn(ResourceRetrievalWarning(frozenset({space}), "space", str(e)), stacklevel=2)
367
356
  return []