cognite-neat 0.103.0__py3-none-any.whl → 0.104.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/_graph/extractors/_mock_graph_generator.py +1 -1
- cognite/neat/_graph/transformers/_base.py +109 -1
- cognite/neat/_graph/transformers/_classic_cdf.py +4 -0
- cognite/neat/_graph/transformers/_prune_graph.py +103 -47
- cognite/neat/_graph/transformers/_rdfpath.py +41 -17
- cognite/neat/_graph/transformers/_value_type.py +119 -154
- cognite/neat/_issues/_base.py +35 -8
- cognite/neat/_issues/warnings/_resources.py +1 -1
- cognite/neat/_rules/_shared.py +18 -34
- cognite/neat/_rules/exporters/_base.py +28 -2
- cognite/neat/_rules/exporters/_rules2dms.py +4 -0
- cognite/neat/_rules/exporters/_rules2excel.py +11 -0
- cognite/neat/_rules/exporters/_rules2instance_template.py +4 -0
- cognite/neat/_rules/exporters/_rules2ontology.py +13 -1
- cognite/neat/_rules/exporters/_rules2yaml.py +4 -0
- cognite/neat/_rules/importers/_base.py +9 -0
- cognite/neat/_rules/importers/_dms2rules.py +17 -5
- cognite/neat/_rules/importers/_dtdl2rules/dtdl_importer.py +5 -2
- cognite/neat/_rules/importers/_rdf/_base.py +10 -8
- cognite/neat/_rules/importers/_rdf/_imf2rules.py +4 -0
- cognite/neat/_rules/importers/_rdf/_inference2rules.py +7 -0
- cognite/neat/_rules/importers/_rdf/_owl2rules.py +4 -0
- cognite/neat/_rules/importers/_spreadsheet2rules.py +17 -8
- cognite/neat/_rules/importers/_yaml2rules.py +21 -7
- cognite/neat/_rules/models/_base_input.py +1 -1
- cognite/neat/_rules/models/_base_rules.py +5 -0
- cognite/neat/_rules/models/dms/_rules.py +4 -0
- cognite/neat/_rules/models/dms/_rules_input.py +9 -0
- cognite/neat/_rules/models/dms/_validation.py +2 -0
- cognite/neat/_rules/models/entities/_single_value.py +25 -5
- cognite/neat/_rules/models/information/_rules.py +4 -0
- cognite/neat/_rules/models/information/_rules_input.py +9 -0
- cognite/neat/_rules/models/mapping/_classic2core.py +2 -5
- cognite/neat/_rules/transformers/__init__.py +5 -4
- cognite/neat/_rules/transformers/_base.py +41 -65
- cognite/neat/_rules/transformers/_converters.py +149 -62
- cognite/neat/_rules/transformers/_mapping.py +17 -12
- cognite/neat/_rules/transformers/_verification.py +50 -37
- cognite/neat/_session/_base.py +32 -121
- cognite/neat/_session/_inspect.py +3 -3
- cognite/neat/_session/_mapping.py +17 -105
- cognite/neat/_session/_prepare.py +36 -254
- cognite/neat/_session/_read.py +11 -130
- cognite/neat/_session/_set.py +6 -30
- cognite/neat/_session/_show.py +49 -30
- cognite/neat/_session/_state.py +49 -107
- cognite/neat/_session/_to.py +42 -31
- cognite/neat/_shared.py +23 -2
- cognite/neat/_store/_provenance.py +3 -82
- cognite/neat/_store/_rules_store.py +372 -10
- cognite/neat/_store/exceptions.py +23 -0
- cognite/neat/_utils/graph_transformations_report.py +36 -0
- cognite/neat/_utils/io_.py +11 -0
- cognite/neat/_utils/rdf_.py +8 -0
- cognite/neat/_utils/spreadsheet.py +5 -4
- cognite/neat/_version.py +1 -1
- cognite/neat/_workflows/steps/lib/current/rules_exporter.py +7 -7
- cognite/neat/_workflows/steps/lib/current/rules_importer.py +24 -99
- {cognite_neat-0.103.0.dist-info → cognite_neat-0.104.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.103.0.dist-info → cognite_neat-0.104.0.dist-info}/RECORD +63 -61
- cognite/neat/_rules/transformers/_pipelines.py +0 -70
- {cognite_neat-0.103.0.dist-info → cognite_neat-0.104.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.103.0.dist-info → cognite_neat-0.104.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.103.0.dist-info → cognite_neat-0.104.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,20 +1,382 @@
|
|
|
1
|
-
|
|
1
|
+
import hashlib
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from collections.abc import Callable, Hashable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from functools import partial
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import rdflib
|
|
11
|
+
|
|
12
|
+
from cognite.neat._client import NeatClient
|
|
13
|
+
from cognite.neat._constants import DEFAULT_NAMESPACE
|
|
14
|
+
from cognite.neat._issues import IssueList, catch_issues
|
|
15
|
+
from cognite.neat._issues.errors import NeatValueError
|
|
16
|
+
from cognite.neat._rules._shared import ReadRules, Rules, T_VerifiedRules, VerifiedRules
|
|
2
17
|
from cognite.neat._rules.exporters import BaseExporter
|
|
18
|
+
from cognite.neat._rules.exporters._base import CDFExporter, T_Export
|
|
3
19
|
from cognite.neat._rules.importers import BaseImporter
|
|
20
|
+
from cognite.neat._rules.models import DMSRules, InformationRules
|
|
21
|
+
from cognite.neat._rules.models._base_input import InputRules
|
|
4
22
|
from cognite.neat._rules.transformers import RulesTransformer
|
|
23
|
+
from cognite.neat._utils.upload import UploadResultList
|
|
24
|
+
|
|
25
|
+
from ._provenance import EMPTY_ENTITY, UNKNOWN_AGENT, Activity, Agent, Change, Entity, Provenance
|
|
26
|
+
from .exceptions import EmptyStore, InvalidInputOperation
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class ModelEntity(Entity):
|
|
31
|
+
result: Rules | None = None
|
|
5
32
|
|
|
6
|
-
|
|
33
|
+
@property
|
|
34
|
+
def display_name(self) -> str:
|
|
35
|
+
if self.result is None:
|
|
36
|
+
return "Failed"
|
|
37
|
+
if isinstance(self.result, ReadRules):
|
|
38
|
+
if self.result.rules is None:
|
|
39
|
+
return "FailedRead"
|
|
40
|
+
return self.result.rules.display_type_name()
|
|
41
|
+
else:
|
|
42
|
+
return self.result.display_type_name()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class OutcomeEntity(Entity):
|
|
47
|
+
result: UploadResultList | Path | str | None = None
|
|
7
48
|
|
|
8
49
|
|
|
9
50
|
class NeatRulesStore:
|
|
10
|
-
def __init__(self):
|
|
11
|
-
self.
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.provenance = Provenance()
|
|
53
|
+
self.exports_by_source_entity_id: dict[rdflib.URIRef, list[Change]] = defaultdict(list)
|
|
54
|
+
self.pruned_by_source_entity_id: dict[rdflib.URIRef, list[Provenance]] = defaultdict(list)
|
|
55
|
+
self._last_outcome: UploadResultList | None = None
|
|
56
|
+
self._iteration_by_id: dict[Hashable, int] = {}
|
|
57
|
+
|
|
58
|
+
def calculate_provenance_hash(self, shorten: bool = True) -> str:
|
|
59
|
+
sha256_hash = hashlib.sha256()
|
|
60
|
+
for change in self.provenance:
|
|
61
|
+
for id_ in [change.agent.id_, change.activity.id_, change.target_entity.id_, change.source_entity.id_]:
|
|
62
|
+
sha256_hash.update(str(id_).encode("utf-8"))
|
|
63
|
+
calculated_hash = sha256_hash.hexdigest()
|
|
64
|
+
if shorten:
|
|
65
|
+
return calculated_hash[:8]
|
|
66
|
+
return calculated_hash
|
|
67
|
+
|
|
68
|
+
def import_(self, importer: BaseImporter) -> IssueList:
|
|
69
|
+
agent = importer.agent
|
|
70
|
+
source_entity = Entity(
|
|
71
|
+
was_attributed_to=UNKNOWN_AGENT,
|
|
72
|
+
id_=importer.source_uri,
|
|
73
|
+
)
|
|
74
|
+
return self._do_activity(importer.to_rules, agent, source_entity, importer.description)[1]
|
|
75
|
+
|
|
76
|
+
def transform(self, *transformer: RulesTransformer) -> IssueList:
|
|
77
|
+
if not self.provenance:
|
|
78
|
+
raise EmptyStore()
|
|
79
|
+
|
|
80
|
+
all_issues = IssueList()
|
|
81
|
+
for item in transformer:
|
|
82
|
+
last_change = self.provenance[-1]
|
|
83
|
+
source_entity = last_change.target_entity
|
|
84
|
+
if not isinstance(source_entity, ModelEntity):
|
|
85
|
+
# Todo: Provenance should be of an entity type
|
|
86
|
+
raise ValueError("Bug in neat: The last entity in the provenance is not a model entity.")
|
|
87
|
+
transformer_input = source_entity.result
|
|
88
|
+
|
|
89
|
+
if not item.is_valid_input(transformer_input):
|
|
90
|
+
raise InvalidInputOperation(expected=item.transform_type_hint(), got=type(transformer_input))
|
|
91
|
+
|
|
92
|
+
transform_issues = self._do_activity(
|
|
93
|
+
partial(item.transform, rules=transformer_input),
|
|
94
|
+
item.agent,
|
|
95
|
+
source_entity,
|
|
96
|
+
item.description,
|
|
97
|
+
)[1]
|
|
98
|
+
all_issues.extend(transform_issues)
|
|
99
|
+
return all_issues
|
|
100
|
+
|
|
101
|
+
def export(self, exporter: BaseExporter[T_VerifiedRules, T_Export]) -> T_Export:
|
|
102
|
+
last_change = self.provenance[-1]
|
|
103
|
+
source_entity = last_change.target_entity
|
|
104
|
+
if not isinstance(source_entity, ModelEntity):
|
|
105
|
+
# Todo: Provenance should be of an entity type
|
|
106
|
+
raise ValueError("Bug in neat: The last entity in the provenance is not a model entity.")
|
|
107
|
+
expected_types = exporter.source_types()
|
|
108
|
+
if not any(isinstance(source_entity.result, type_) for type_ in expected_types):
|
|
109
|
+
raise InvalidInputOperation(expected=expected_types, got=type(source_entity.result))
|
|
110
|
+
|
|
111
|
+
agent = exporter.agent
|
|
112
|
+
start = datetime.now(timezone.utc)
|
|
113
|
+
issue_list = IssueList()
|
|
114
|
+
with catch_issues(issue_list) as _:
|
|
115
|
+
# Validate the type of the result
|
|
116
|
+
result = exporter.export(source_entity.result) # type: ignore[arg-type]
|
|
117
|
+
end = datetime.now(timezone.utc)
|
|
118
|
+
target_id = DEFAULT_NAMESPACE["export-result"]
|
|
119
|
+
activity = Activity(
|
|
120
|
+
was_associated_with=agent,
|
|
121
|
+
ended_at_time=end,
|
|
122
|
+
started_at_time=start,
|
|
123
|
+
used=source_entity,
|
|
124
|
+
)
|
|
125
|
+
target_entity = OutcomeEntity(
|
|
126
|
+
was_attributed_to=agent,
|
|
127
|
+
was_generated_by=activity,
|
|
128
|
+
result=type(result).__name__,
|
|
129
|
+
issues=issue_list,
|
|
130
|
+
id_=target_id,
|
|
131
|
+
)
|
|
132
|
+
change = Change(
|
|
133
|
+
agent=agent,
|
|
134
|
+
activity=activity,
|
|
135
|
+
target_entity=target_entity,
|
|
136
|
+
description=exporter.description,
|
|
137
|
+
source_entity=source_entity,
|
|
138
|
+
)
|
|
139
|
+
self.exports_by_source_entity_id[source_entity.id_].append(change)
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
def export_to_file(self, exporter: BaseExporter, path: Path) -> None:
|
|
143
|
+
last_change = self.provenance[-1]
|
|
144
|
+
source_entity = last_change.target_entity
|
|
145
|
+
if not isinstance(source_entity, ModelEntity):
|
|
146
|
+
# Todo: Provenance should be of an entity type
|
|
147
|
+
raise ValueError("Bug in neat: The last entity in the provenance is not a model entity.")
|
|
148
|
+
expected_types = exporter.source_types()
|
|
149
|
+
if not any(isinstance(source_entity.result, type_) for type_ in expected_types):
|
|
150
|
+
raise InvalidInputOperation(expected=expected_types, got=type(source_entity.result))
|
|
151
|
+
target_id = DEFAULT_NAMESPACE[path.name]
|
|
152
|
+
agent = exporter.agent
|
|
153
|
+
start = datetime.now(timezone.utc)
|
|
154
|
+
issue_list = IssueList()
|
|
155
|
+
with catch_issues(issue_list) as _:
|
|
156
|
+
exporter.export_to_file(source_entity.result, path)
|
|
157
|
+
end = datetime.now(timezone.utc)
|
|
158
|
+
|
|
159
|
+
activity = Activity(
|
|
160
|
+
was_associated_with=agent,
|
|
161
|
+
ended_at_time=end,
|
|
162
|
+
started_at_time=start,
|
|
163
|
+
used=source_entity,
|
|
164
|
+
)
|
|
165
|
+
target_entity = OutcomeEntity(
|
|
166
|
+
was_attributed_to=agent,
|
|
167
|
+
was_generated_by=activity,
|
|
168
|
+
result=path,
|
|
169
|
+
issues=issue_list,
|
|
170
|
+
id_=target_id,
|
|
171
|
+
)
|
|
172
|
+
change = Change(
|
|
173
|
+
agent=agent,
|
|
174
|
+
activity=activity,
|
|
175
|
+
target_entity=target_entity,
|
|
176
|
+
description=exporter.description,
|
|
177
|
+
source_entity=source_entity,
|
|
178
|
+
)
|
|
179
|
+
self.exports_by_source_entity_id[source_entity.id_].append(change)
|
|
180
|
+
|
|
181
|
+
def export_to_cdf(self, exporter: CDFExporter, client: NeatClient, dry_run: bool) -> UploadResultList:
|
|
182
|
+
last_change = self.provenance[-1]
|
|
183
|
+
source_entity = last_change.target_entity
|
|
184
|
+
if not isinstance(source_entity, ModelEntity):
|
|
185
|
+
# Todo: Provenance should be of an entity type
|
|
186
|
+
raise ValueError("Bug in neat: The last entity in the provenance is not a model entity.")
|
|
187
|
+
expected_types = exporter.source_types()
|
|
188
|
+
if not any(isinstance(source_entity.result, type_) for type_ in expected_types):
|
|
189
|
+
raise InvalidInputOperation(expected=expected_types, got=type(source_entity.result))
|
|
190
|
+
|
|
191
|
+
agent = exporter.agent
|
|
192
|
+
start = datetime.now(timezone.utc)
|
|
193
|
+
target_id = DEFAULT_NAMESPACE["upload-result"]
|
|
194
|
+
issue_list = IssueList()
|
|
195
|
+
result: UploadResultList | None = None
|
|
196
|
+
with catch_issues(issue_list) as _:
|
|
197
|
+
result = exporter.export_to_cdf(source_entity.result, client, dry_run)
|
|
198
|
+
end = datetime.now(timezone.utc)
|
|
199
|
+
|
|
200
|
+
activity = Activity(
|
|
201
|
+
was_associated_with=agent,
|
|
202
|
+
ended_at_time=end,
|
|
203
|
+
started_at_time=start,
|
|
204
|
+
used=source_entity,
|
|
205
|
+
)
|
|
206
|
+
target_entity = OutcomeEntity(
|
|
207
|
+
was_attributed_to=agent,
|
|
208
|
+
was_generated_by=activity,
|
|
209
|
+
result=result,
|
|
210
|
+
issues=issue_list,
|
|
211
|
+
id_=target_id,
|
|
212
|
+
)
|
|
213
|
+
change = Change(
|
|
214
|
+
agent=agent,
|
|
215
|
+
activity=activity,
|
|
216
|
+
target_entity=target_entity,
|
|
217
|
+
description=exporter.description,
|
|
218
|
+
source_entity=source_entity,
|
|
219
|
+
)
|
|
220
|
+
self.exports_by_source_entity_id[source_entity.id_].append(change)
|
|
221
|
+
self._last_outcome = result
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
def prune_until_compatible(self, transformer: RulesTransformer) -> list[Change]:
|
|
225
|
+
"""Prune the provenance until the last successful entity is compatible with the transformer.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
transformer: The transformer to check compatibility with.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
The changes that were pruned.
|
|
232
|
+
"""
|
|
233
|
+
pruned_candidates: list[Change] = []
|
|
234
|
+
for change in reversed(self.provenance):
|
|
235
|
+
if not isinstance(change.target_entity, ModelEntity):
|
|
236
|
+
continue
|
|
237
|
+
if not transformer.is_valid_input(change.target_entity.result):
|
|
238
|
+
pruned_candidates.append(change)
|
|
239
|
+
else:
|
|
240
|
+
break
|
|
241
|
+
else:
|
|
242
|
+
raise NeatValueError("No compatible entity found in the provenance.")
|
|
243
|
+
if not pruned_candidates:
|
|
244
|
+
return []
|
|
245
|
+
self.provenance = self.provenance[: -len(pruned_candidates)]
|
|
246
|
+
pruned_candidates.reverse()
|
|
247
|
+
self.pruned_by_source_entity_id[self.provenance[-1].target_entity.id_].append(Provenance(pruned_candidates))
|
|
248
|
+
return pruned_candidates
|
|
249
|
+
|
|
250
|
+
def _export(self, action: Callable[[Any], Any], agent: Agent, description: str) -> Any:
|
|
251
|
+
last_entity: ModelEntity | None = None
|
|
252
|
+
for change in reversed(self.provenance):
|
|
253
|
+
if isinstance(change.target_entity, ModelEntity) and isinstance(change.target_entity.result, DMSRules):
|
|
254
|
+
last_entity = change.target_entity
|
|
255
|
+
break
|
|
256
|
+
if last_entity is None:
|
|
257
|
+
raise NeatValueError("No verified DMS rules found in the provenance.")
|
|
258
|
+
rules = last_entity.result
|
|
259
|
+
result, _ = self._do_activity(lambda: action(rules), agent, last_entity, description)
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
def _do_activity(
|
|
263
|
+
self, action: Callable[[], Rules | None], agent: Agent, source_entity: Entity, description: str
|
|
264
|
+
) -> tuple[Any, IssueList]:
|
|
265
|
+
start = datetime.now(timezone.utc)
|
|
266
|
+
issue_list = IssueList()
|
|
267
|
+
result: Rules | None = None
|
|
268
|
+
with catch_issues(issue_list) as _:
|
|
269
|
+
result = action()
|
|
270
|
+
end = datetime.now(timezone.utc)
|
|
271
|
+
|
|
272
|
+
activity = Activity(
|
|
273
|
+
was_associated_with=agent,
|
|
274
|
+
ended_at_time=end,
|
|
275
|
+
started_at_time=start,
|
|
276
|
+
used=source_entity,
|
|
277
|
+
)
|
|
278
|
+
target_entity = ModelEntity(
|
|
279
|
+
was_attributed_to=agent,
|
|
280
|
+
was_generated_by=activity,
|
|
281
|
+
result=result,
|
|
282
|
+
issues=issue_list,
|
|
283
|
+
id_=self._create_id(result),
|
|
284
|
+
)
|
|
285
|
+
change = Change(
|
|
286
|
+
agent=agent,
|
|
287
|
+
activity=activity,
|
|
288
|
+
target_entity=target_entity,
|
|
289
|
+
description=description,
|
|
290
|
+
source_entity=source_entity,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
self.provenance.append(change)
|
|
294
|
+
return result, issue_list
|
|
295
|
+
|
|
296
|
+
def _create_id(self, result: Any) -> rdflib.URIRef:
|
|
297
|
+
identifier: rdflib.URIRef
|
|
298
|
+
if result is None:
|
|
299
|
+
identifier = EMPTY_ENTITY.id_
|
|
300
|
+
elif isinstance(result, ReadRules):
|
|
301
|
+
if result.rules is None:
|
|
302
|
+
identifier = EMPTY_ENTITY.id_
|
|
303
|
+
else:
|
|
304
|
+
identifier = result.rules.metadata.identifier
|
|
305
|
+
elif isinstance(result, VerifiedRules):
|
|
306
|
+
identifier = result.metadata.identifier
|
|
307
|
+
else:
|
|
308
|
+
identifier = DEFAULT_NAMESPACE["unknown-entity"]
|
|
309
|
+
|
|
310
|
+
if identifier not in self._iteration_by_id:
|
|
311
|
+
self._iteration_by_id[identifier] = 1
|
|
312
|
+
return identifier
|
|
313
|
+
self._iteration_by_id[identifier] += 1
|
|
314
|
+
return identifier + f"/Iteration_{self._iteration_by_id[identifier]}"
|
|
315
|
+
|
|
316
|
+
def get_last_entity(self) -> ModelEntity:
|
|
317
|
+
if not self.provenance:
|
|
318
|
+
raise NeatValueError("No entity found in the provenance.")
|
|
319
|
+
return cast(ModelEntity, self.provenance[-1].target_entity)
|
|
320
|
+
|
|
321
|
+
def get_last_successful_entity(self) -> ModelEntity:
|
|
322
|
+
for change in reversed(self.provenance):
|
|
323
|
+
if isinstance(change.target_entity, ModelEntity) and change.target_entity.result:
|
|
324
|
+
return change.target_entity
|
|
325
|
+
raise NeatValueError("No successful entity found in the provenance.")
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def has_unverified_rules(self) -> bool:
|
|
329
|
+
return any(
|
|
330
|
+
isinstance(change.target_entity, ModelEntity)
|
|
331
|
+
and isinstance(change.target_entity.result, ReadRules)
|
|
332
|
+
and change.target_entity.result.rules is not None
|
|
333
|
+
for change in self.provenance
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def has_verified_rules(self) -> bool:
|
|
338
|
+
return any(
|
|
339
|
+
isinstance(change.target_entity, ModelEntity)
|
|
340
|
+
and isinstance(change.target_entity.result, DMSRules | InformationRules)
|
|
341
|
+
for change in self.provenance
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def last_unverified_rule(self) -> InputRules:
|
|
346
|
+
for change in reversed(self.provenance):
|
|
347
|
+
if (
|
|
348
|
+
isinstance(change.target_entity, ModelEntity)
|
|
349
|
+
and isinstance(change.target_entity.result, ReadRules)
|
|
350
|
+
and change.target_entity.result.rules is not None
|
|
351
|
+
):
|
|
352
|
+
return change.target_entity.result.rules
|
|
353
|
+
|
|
354
|
+
raise NeatValueError("No unverified rule found in the provenance.")
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def last_verified_rule(self) -> DMSRules | InformationRules:
|
|
358
|
+
for change in reversed(self.provenance):
|
|
359
|
+
if isinstance(change.target_entity, ModelEntity) and isinstance(
|
|
360
|
+
change.target_entity.result, DMSRules | InformationRules
|
|
361
|
+
):
|
|
362
|
+
return change.target_entity.result
|
|
363
|
+
raise NeatValueError("No verified rule found in the provenance.")
|
|
12
364
|
|
|
13
|
-
|
|
14
|
-
|
|
365
|
+
@property
|
|
366
|
+
def last_verified_dms_rules(self) -> DMSRules:
|
|
367
|
+
for change in reversed(self.provenance):
|
|
368
|
+
if isinstance(change.target_entity, ModelEntity) and isinstance(change.target_entity.result, DMSRules):
|
|
369
|
+
return change.target_entity.result
|
|
370
|
+
raise NeatValueError("No verified DMS rules found in the provenance.")
|
|
15
371
|
|
|
16
|
-
|
|
17
|
-
|
|
372
|
+
@property
|
|
373
|
+
def last_issues(self) -> IssueList:
|
|
374
|
+
if not self.provenance:
|
|
375
|
+
raise NeatValueError("No issues found in the provenance.")
|
|
376
|
+
return self.provenance[-1].target_entity.issues
|
|
18
377
|
|
|
19
|
-
|
|
20
|
-
|
|
378
|
+
@property
|
|
379
|
+
def last_outcome(self) -> UploadResultList:
|
|
380
|
+
if self._last_outcome is not None:
|
|
381
|
+
return self._last_outcome
|
|
382
|
+
raise NeatValueError("No outcome found in the provenance.")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""These are special exceptions that are used by the store to signal invalid transformers"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NeatStoreError(Exception):
|
|
7
|
+
"""Base class for all exceptions in the store module"""
|
|
8
|
+
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class InvalidInputOperation(NeatStoreError, RuntimeError):
|
|
14
|
+
"""Raised when an invalid operation is attempted"""
|
|
15
|
+
|
|
16
|
+
expected: tuple[type, ...]
|
|
17
|
+
got: type
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EmptyStore(NeatStoreError, RuntimeError):
|
|
21
|
+
"""Raised when the store is empty"""
|
|
22
|
+
|
|
23
|
+
...
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from cognite.neat._shared import NeatList, NeatObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class GraphTransformationResult(NeatObject, ABC):
|
|
10
|
+
name: str
|
|
11
|
+
affected_nodes_count: int | None = None
|
|
12
|
+
added: int | None = None
|
|
13
|
+
removed: int | None = None
|
|
14
|
+
skipped: int | None = None
|
|
15
|
+
modified: int | None = None
|
|
16
|
+
|
|
17
|
+
def dump(self, aggregate: bool = True) -> dict[str, Any]:
|
|
18
|
+
output: dict[str, Any] = {"name": self.name}
|
|
19
|
+
if self.added:
|
|
20
|
+
output["added"] = self.added
|
|
21
|
+
if self.removed:
|
|
22
|
+
output["removed"] = self.removed
|
|
23
|
+
if self.skipped:
|
|
24
|
+
output["skipped"] = self.skipped
|
|
25
|
+
if self.affected_nodes_count:
|
|
26
|
+
output["affected nodes"] = self.affected_nodes_count
|
|
27
|
+
if self.modified:
|
|
28
|
+
output["modified instances"] = self.modified
|
|
29
|
+
return output
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GraphTransformationResultList(NeatList[GraphTransformationResult]):
|
|
33
|
+
def _repr_html_(self) -> str:
|
|
34
|
+
df = self.to_pandas().fillna(0)
|
|
35
|
+
df = df.style.format({column: "{:,.0f}".format for column in df.select_dtypes(include="number").columns})
|
|
36
|
+
return df._repr_html_() # type: ignore[attr-defined]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
# Spaces are allowed, but we replace them as well
|
|
4
|
+
_ILLEGAL_CHARACTERS = re.compile(r"[<>:\"/\\|?*\s]")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def to_directory_compatible(text: str) -> str:
|
|
8
|
+
"""Convert a string to be compatible with directory names on all platforms"""
|
|
9
|
+
cleaned = _ILLEGAL_CHARACTERS.sub("_", text)
|
|
10
|
+
# Replace multiple underscores with a single one
|
|
11
|
+
return re.sub(r"_+", "_", cleaned)
|
cognite/neat/_utils/rdf_.py
CHANGED
|
@@ -257,3 +257,11 @@ def remove_instance_ids_in_batch(graph: Graph, instance_ids: Iterable[URIRef], b
|
|
|
257
257
|
check_commit()
|
|
258
258
|
|
|
259
259
|
check_commit(force_commit=True)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def uri_display_name(thing: URIRef) -> str:
|
|
263
|
+
if "https://cognitedata.com/dms/data-model/" in thing:
|
|
264
|
+
return "DMS(" + ",".join(thing.replace("https://cognitedata.com/dms/data-model/", "").split("/")) + ")"
|
|
265
|
+
elif "http://purl.org/cognite/neat/data-model/" in thing:
|
|
266
|
+
return "NEAT(" + ",".join(thing.replace("http://purl.org/cognite/neat/data-model/", "").split("/")) + ")"
|
|
267
|
+
return remove_namespace_from_uri(thing)
|
|
@@ -14,7 +14,7 @@ class SpreadsheetRead:
|
|
|
14
14
|
such that the error/warning messages are accurate.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
header_row: int =
|
|
17
|
+
header_row: int = 1
|
|
18
18
|
empty_rows: list[int] = field(default_factory=list)
|
|
19
19
|
is_one_indexed: bool = True
|
|
20
20
|
|
|
@@ -22,13 +22,13 @@ class SpreadsheetRead:
|
|
|
22
22
|
self.empty_rows = sorted(self.empty_rows)
|
|
23
23
|
|
|
24
24
|
def adjusted_row_number(self, row_no: int) -> int:
|
|
25
|
-
output = row_no
|
|
25
|
+
output = row_no
|
|
26
26
|
for empty_row in self.empty_rows:
|
|
27
27
|
if empty_row <= output:
|
|
28
28
|
output += 1
|
|
29
29
|
else:
|
|
30
30
|
break
|
|
31
|
-
return output
|
|
31
|
+
return output + self.header_row + (1 if self.is_one_indexed else 0)
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@overload
|
|
@@ -71,7 +71,8 @@ def read_individual_sheet(
|
|
|
71
71
|
raw["Value Type"] = raw["Value Type"].replace(float("nan"), "#N/A")
|
|
72
72
|
output = raw.replace(float("nan"), None).to_dict(orient="records")
|
|
73
73
|
if return_read_info:
|
|
74
|
-
|
|
74
|
+
# If no rows are skipped, row 1 is the header row.
|
|
75
|
+
return output, SpreadsheetRead(header_row=skiprows + 1, empty_rows=empty_rows, is_one_indexed=True)
|
|
75
76
|
return output
|
|
76
77
|
|
|
77
78
|
|
cognite/neat/_version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.104.0"
|
|
2
2
|
__engine__ = "^2.0.3"
|
|
@@ -89,7 +89,7 @@ class DeleteDataModelFromCDF(Step):
|
|
|
89
89
|
if isinstance(input_rules, DMSRules):
|
|
90
90
|
dms_rules = input_rules
|
|
91
91
|
elif isinstance(input_rules, InformationRules):
|
|
92
|
-
dms_rules = InformationToDMS().transform(input_rules)
|
|
92
|
+
dms_rules = InformationToDMS().transform(input_rules)
|
|
93
93
|
else:
|
|
94
94
|
raise NotImplementedError(f"Unsupported rules type {type(input_rules)}")
|
|
95
95
|
|
|
@@ -201,7 +201,7 @@ class RulesToDMS(Step):
|
|
|
201
201
|
if isinstance(input_rules, DMSRules):
|
|
202
202
|
dms_rules = input_rules
|
|
203
203
|
elif isinstance(input_rules, InformationRules):
|
|
204
|
-
dms_rules = InformationToDMS().transform(input_rules)
|
|
204
|
+
dms_rules = InformationToDMS().transform(input_rules)
|
|
205
205
|
else:
|
|
206
206
|
raise NotImplementedError(f"Unsupported rules type {type(input_rules)}")
|
|
207
207
|
|
|
@@ -324,12 +324,12 @@ class RulesToExcel(Step):
|
|
|
324
324
|
...
|
|
325
325
|
elif output_role is RoleTypes.dms:
|
|
326
326
|
if isinstance(rule_instance, InformationRules):
|
|
327
|
-
rule_instance = InformationToDMS().transform(rule_instance)
|
|
327
|
+
rule_instance = InformationToDMS().transform(rule_instance)
|
|
328
328
|
else:
|
|
329
329
|
raise NotImplementedError(f"Role {output_role} is not supported for {type(rules).__name__} rules")
|
|
330
330
|
elif output_role is RoleTypes.information:
|
|
331
331
|
if isinstance(rule_instance, DMSRules):
|
|
332
|
-
rule_instance = DMSToInformation().transform(rule_instance)
|
|
332
|
+
rule_instance = DMSToInformation().transform(rule_instance)
|
|
333
333
|
else:
|
|
334
334
|
raise NotImplementedError(f"Role {output_role} is not supported for {type(rules).__name__} rules")
|
|
335
335
|
else:
|
|
@@ -394,7 +394,7 @@ class RulesToOntology(Step):
|
|
|
394
394
|
|
|
395
395
|
input_rules = rules.information or rules.dms
|
|
396
396
|
if isinstance(input_rules, DMSRules):
|
|
397
|
-
info_rules = DMSToInformation().transform(input_rules)
|
|
397
|
+
info_rules = DMSToInformation().transform(input_rules)
|
|
398
398
|
elif isinstance(input_rules, InformationRules):
|
|
399
399
|
info_rules = input_rules
|
|
400
400
|
else:
|
|
@@ -453,7 +453,7 @@ class RulesToSHACL(Step):
|
|
|
453
453
|
|
|
454
454
|
input_rules = rules.information or rules.dms
|
|
455
455
|
if isinstance(input_rules, DMSRules):
|
|
456
|
-
info_rules = DMSToInformation().transform(input_rules)
|
|
456
|
+
info_rules = DMSToInformation().transform(input_rules)
|
|
457
457
|
elif isinstance(input_rules, InformationRules):
|
|
458
458
|
info_rules = input_rules
|
|
459
459
|
else:
|
|
@@ -511,7 +511,7 @@ class RulesToSemanticDataModel(Step):
|
|
|
511
511
|
storage_path.parent.mkdir(parents=True, exist_ok=True)
|
|
512
512
|
input_rules = rules.information or rules.dms
|
|
513
513
|
if isinstance(input_rules, DMSRules):
|
|
514
|
-
info_rules = DMSToInformation().transform(input_rules)
|
|
514
|
+
info_rules = DMSToInformation().transform(input_rules)
|
|
515
515
|
elif isinstance(input_rules, InformationRules):
|
|
516
516
|
info_rules = input_rules
|
|
517
517
|
else:
|