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.
- cognite/neat/_client/__init__.py +4 -0
- cognite/neat/_client/_api/data_modeling_loaders.py +512 -0
- cognite/neat/_client/_api/schema.py +50 -0
- cognite/neat/_client/_api_client.py +17 -0
- cognite/neat/_client/data_classes/__init__.py +0 -0
- cognite/neat/{_utils/cdf/data_classes.py → _client/data_classes/data_modeling.py} +8 -135
- cognite/neat/{_rules/models/dms/_schema.py → _client/data_classes/schema.py} +21 -281
- cognite/neat/_graph/_shared.py +14 -15
- cognite/neat/_graph/extractors/_classic_cdf/_assets.py +14 -154
- cognite/neat/_graph/extractors/_classic_cdf/_base.py +154 -7
- cognite/neat/_graph/extractors/_classic_cdf/_classic.py +23 -12
- cognite/neat/_graph/extractors/_classic_cdf/_data_sets.py +17 -92
- cognite/neat/_graph/extractors/_classic_cdf/_events.py +13 -162
- cognite/neat/_graph/extractors/_classic_cdf/_files.py +15 -179
- cognite/neat/_graph/extractors/_classic_cdf/_labels.py +32 -100
- cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +27 -178
- cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +14 -139
- cognite/neat/_graph/extractors/_classic_cdf/_timeseries.py +15 -173
- cognite/neat/_graph/extractors/_rdf_file.py +6 -7
- cognite/neat/_graph/queries/_base.py +17 -1
- cognite/neat/_graph/transformers/_classic_cdf.py +50 -134
- cognite/neat/_graph/transformers/_prune_graph.py +1 -1
- cognite/neat/_graph/transformers/_rdfpath.py +1 -1
- cognite/neat/_issues/warnings/__init__.py +6 -0
- cognite/neat/_issues/warnings/_external.py +8 -0
- cognite/neat/_issues/warnings/_properties.py +16 -0
- cognite/neat/_rules/_constants.py +7 -6
- cognite/neat/_rules/analysis/_base.py +8 -4
- cognite/neat/_rules/exporters/_base.py +3 -4
- cognite/neat/_rules/exporters/_rules2dms.py +29 -40
- cognite/neat/_rules/importers/_dms2rules.py +4 -5
- cognite/neat/_rules/importers/_rdf/_inference2rules.py +25 -33
- cognite/neat/_rules/models/__init__.py +1 -1
- cognite/neat/_rules/models/_base_rules.py +22 -12
- cognite/neat/_rules/models/dms/__init__.py +2 -2
- cognite/neat/_rules/models/dms/_exporter.py +15 -20
- cognite/neat/_rules/models/dms/_rules.py +48 -3
- cognite/neat/_rules/models/dms/_rules_input.py +52 -8
- cognite/neat/_rules/models/dms/_validation.py +10 -5
- cognite/neat/_rules/models/entities/_single_value.py +32 -4
- cognite/neat/_rules/models/information/_rules.py +0 -8
- cognite/neat/_rules/models/mapping/__init__.py +2 -3
- cognite/neat/_rules/models/mapping/_classic2core.py +36 -146
- cognite/neat/_rules/models/mapping/_classic2core.yaml +339 -0
- cognite/neat/_rules/transformers/__init__.py +2 -2
- cognite/neat/_rules/transformers/_converters.py +110 -11
- cognite/neat/_rules/transformers/_mapping.py +105 -30
- cognite/neat/_rules/transformers/_verification.py +5 -2
- cognite/neat/_session/_base.py +49 -8
- cognite/neat/_session/_drop.py +35 -0
- cognite/neat/_session/_inspect.py +17 -5
- cognite/neat/_session/_mapping.py +39 -0
- cognite/neat/_session/_prepare.py +218 -23
- cognite/neat/_session/_read.py +49 -12
- cognite/neat/_session/_to.py +3 -3
- cognite/neat/_store/_base.py +27 -24
- cognite/neat/_utils/rdf_.py +28 -1
- cognite/neat/_version.py +1 -1
- cognite/neat/_workflows/steps/lib/current/rules_exporter.py +8 -3
- cognite/neat/_workflows/steps/lib/current/rules_importer.py +4 -1
- cognite/neat/_workflows/steps/lib/current/rules_validator.py +3 -2
- {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/METADATA +3 -3
- {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/RECORD +67 -64
- cognite/neat/_rules/models/mapping/_base.py +0 -131
- cognite/neat/_utils/cdf/loaders/__init__.py +0 -25
- cognite/neat/_utils/cdf/loaders/_base.py +0 -54
- cognite/neat/_utils/cdf/loaders/_data_modeling.py +0 -339
- cognite/neat/_utils/cdf/loaders/_ingestion.py +0 -167
- /cognite/neat/{_utils/cdf → _client/_api}/__init__.py +0 -0
- {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.98.0.dist-info → cognite_neat-0.99.0.dist-info}/WHEEL +0 -0
- {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.
|
|
36
|
-
self.parent_prop = parent_prop or DEFAULT_NAMESPACE.
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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.
|
|
275
|
-
self.relationship_target_xid_prop = relationship_target_xid_prop or DEFAULT_NAMESPACE.
|
|
276
|
-
self.asset_xid_property = asset_xid_property or DEFAULT_NAMESPACE.
|
|
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(
|
|
126
|
+
graph.remove((node["subject"], None, None))
|
|
@@ -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
|
|
|
@@ -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() ->
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
318
|
+
def _prepare_exporters(self, rules: DMSRules) -> list[CogniteResourceList]:
|
|
326
319
|
schema = self.export(rules)
|
|
327
|
-
to_export: list[
|
|
320
|
+
to_export: list[CogniteResourceList] = []
|
|
328
321
|
if self.export_components.intersection({"all", "spaces"}):
|
|
329
|
-
to_export.append(
|
|
322
|
+
to_export.append(SpaceApplyList(schema.spaces.values()))
|
|
330
323
|
if self.export_components.intersection({"all", "containers"}):
|
|
331
|
-
to_export.append(
|
|
324
|
+
to_export.append(ContainerApplyList(schema.containers.values()))
|
|
332
325
|
if self.export_components.intersection({"all", "views"}):
|
|
333
|
-
to_export.append(
|
|
326
|
+
to_export.append(ViewApplyList(schema.views.values()))
|
|
334
327
|
if self.export_components.intersection({"all", "data_models"}):
|
|
335
|
-
to_export.append(
|
|
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:
|
|
331
|
+
def _validate(self, loader: DataModelingLoader, items: CogniteResourceList, client: NeatClient) -> IssueList:
|
|
343
332
|
issue_list = IssueList()
|
|
344
|
-
if isinstance(
|
|
333
|
+
if isinstance(items, DataModelApplyList):
|
|
345
334
|
models = cast(list[DataModelApply], items)
|
|
346
|
-
if other_models := self._exist_other_data_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,
|
|
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 =
|
|
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 []
|