cognite-neat 0.88.3__py3-none-any.whl → 0.90.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/_version.py +1 -1
- cognite/neat/constants.py +6 -3
- cognite/neat/graph/extractors/__init__.py +2 -0
- cognite/neat/graph/extractors/_classic_cdf/_base.py +2 -2
- cognite/neat/graph/extractors/_dms.py +158 -0
- cognite/neat/graph/extractors/_mock_graph_generator.py +50 -9
- cognite/neat/graph/loaders/_rdf2dms.py +16 -13
- cognite/neat/graph/models.py +1 -0
- cognite/neat/graph/queries/_base.py +4 -2
- cognite/neat/issues/_base.py +3 -1
- cognite/neat/issues/errors/__init__.py +2 -1
- cognite/neat/issues/errors/_general.py +7 -0
- cognite/neat/issues/warnings/__init__.py +2 -0
- cognite/neat/issues/warnings/_models.py +1 -1
- cognite/neat/issues/warnings/_properties.py +12 -0
- cognite/neat/issues/warnings/user_modeling.py +1 -1
- cognite/neat/rules/_shared.py +49 -6
- cognite/neat/rules/analysis/_base.py +1 -1
- cognite/neat/rules/exporters/_base.py +7 -18
- cognite/neat/rules/exporters/_rules2dms.py +8 -18
- cognite/neat/rules/exporters/_rules2excel.py +5 -12
- cognite/neat/rules/exporters/_rules2ontology.py +9 -19
- cognite/neat/rules/exporters/_rules2yaml.py +3 -6
- cognite/neat/rules/importers/_base.py +7 -52
- cognite/neat/rules/importers/_dms2rules.py +172 -116
- cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +26 -18
- cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +14 -30
- cognite/neat/rules/importers/_rdf/_imf2rules/_imf2classes.py +7 -3
- cognite/neat/rules/importers/_rdf/_imf2rules/_imf2metadata.py +3 -3
- cognite/neat/rules/importers/_rdf/_imf2rules/_imf2properties.py +18 -11
- cognite/neat/rules/importers/_rdf/_imf2rules/_imf2rules.py +9 -18
- cognite/neat/rules/importers/_rdf/_inference2rules.py +37 -65
- cognite/neat/rules/importers/_rdf/_owl2rules/_owl2rules.py +9 -20
- cognite/neat/rules/importers/_rdf/_shared.py +1 -1
- cognite/neat/rules/importers/_spreadsheet2rules.py +22 -86
- cognite/neat/rules/importers/_yaml2rules.py +14 -41
- cognite/neat/rules/models/__init__.py +21 -5
- cognite/neat/rules/models/_base_input.py +162 -0
- cognite/neat/rules/models/{_base.py → _base_rules.py} +1 -12
- cognite/neat/rules/models/asset/__init__.py +5 -2
- cognite/neat/rules/models/asset/_rules.py +2 -20
- cognite/neat/rules/models/asset/_rules_input.py +40 -115
- cognite/neat/rules/models/asset/_validation.py +1 -1
- cognite/neat/rules/models/data_types.py +150 -44
- cognite/neat/rules/models/dms/__init__.py +19 -7
- cognite/neat/rules/models/dms/_exporter.py +82 -39
- cognite/neat/rules/models/dms/_rules.py +42 -155
- cognite/neat/rules/models/dms/_rules_input.py +186 -254
- cognite/neat/rules/models/dms/_serializer.py +44 -3
- cognite/neat/rules/models/dms/_validation.py +3 -4
- cognite/neat/rules/models/domain.py +52 -1
- cognite/neat/rules/models/entities/__init__.py +63 -0
- cognite/neat/rules/models/entities/_constants.py +73 -0
- cognite/neat/rules/models/entities/_loaders.py +76 -0
- cognite/neat/rules/models/entities/_multi_value.py +67 -0
- cognite/neat/rules/models/{entities.py → entities/_single_value.py} +74 -232
- cognite/neat/rules/models/entities/_types.py +86 -0
- cognite/neat/rules/models/{wrapped_entities.py → entities/_wrapped.py} +1 -1
- cognite/neat/rules/models/information/__init__.py +10 -2
- cognite/neat/rules/models/information/_rules.py +3 -14
- cognite/neat/rules/models/information/_rules_input.py +57 -204
- cognite/neat/rules/models/information/_validation.py +1 -1
- cognite/neat/rules/transformers/__init__.py +21 -0
- cognite/neat/rules/transformers/_base.py +69 -3
- cognite/neat/rules/{models/information/_converter.py → transformers/_converters.py} +226 -21
- cognite/neat/rules/transformers/_map_onto.py +97 -0
- cognite/neat/rules/transformers/_pipelines.py +61 -0
- cognite/neat/rules/transformers/_verification.py +136 -0
- cognite/neat/store/_base.py +2 -2
- cognite/neat/store/_provenance.py +10 -1
- cognite/neat/utils/cdf/data_classes.py +20 -0
- cognite/neat/utils/regex_patterns.py +6 -0
- cognite/neat/workflows/steps/lib/current/rules_exporter.py +106 -37
- cognite/neat/workflows/steps/lib/current/rules_importer.py +24 -22
- {cognite_neat-0.88.3.dist-info → cognite_neat-0.90.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.88.3.dist-info → cognite_neat-0.90.0.dist-info}/RECORD +80 -74
- cognite/neat/rules/models/_constants.py +0 -2
- cognite/neat/rules/models/_types/__init__.py +0 -19
- cognite/neat/rules/models/asset/_converter.py +0 -4
- cognite/neat/rules/models/dms/_converter.py +0 -143
- /cognite/neat/rules/models/{_types/_field.py → _types.py} +0 -0
- {cognite_neat-0.88.3.dist-info → cognite_neat-0.90.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.88.3.dist-info → cognite_neat-0.90.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.88.3.dist-info → cognite_neat-0.90.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,40 +1,117 @@
|
|
|
1
1
|
import re
|
|
2
|
+
import warnings
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
2
4
|
from collections import Counter, defaultdict
|
|
3
5
|
from collections.abc import Collection
|
|
4
6
|
from datetime import date, datetime
|
|
5
|
-
from typing import
|
|
7
|
+
from typing import Literal, TypeVar, cast
|
|
6
8
|
|
|
7
9
|
from cognite.client.data_classes import data_modeling as dms
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from cognite.neat.
|
|
10
|
+
from rdflib import Namespace
|
|
11
|
+
|
|
12
|
+
from cognite.neat.constants import DMS_CONTAINER_PROPERTY_SIZE_LIMIT
|
|
13
|
+
from cognite.neat.issues.warnings.user_modeling import ParentInDifferentSpaceWarning
|
|
14
|
+
from cognite.neat.rules._shared import JustRules, OutRules, VerifiedRules
|
|
15
|
+
from cognite.neat.rules.models import (
|
|
16
|
+
AssetRules,
|
|
17
|
+
DMSRules,
|
|
18
|
+
DomainRules,
|
|
11
19
|
ExtensionCategory,
|
|
20
|
+
InformationRules,
|
|
12
21
|
SchemaCompleteness,
|
|
13
22
|
SheetList,
|
|
23
|
+
data_types,
|
|
14
24
|
)
|
|
15
|
-
from cognite.neat.rules.models._constants import DMS_CONTAINER_PROPERTY_SIZE_LIMIT
|
|
16
25
|
from cognite.neat.rules.models.data_types import DataType
|
|
17
|
-
from cognite.neat.rules.models.
|
|
26
|
+
from cognite.neat.rules.models.dms import DMSMetadata, DMSProperty, DMSView
|
|
18
27
|
from cognite.neat.rules.models.entities import (
|
|
19
28
|
AssetEntity,
|
|
20
29
|
AssetFields,
|
|
21
30
|
ClassEntity,
|
|
22
31
|
ContainerEntity,
|
|
23
32
|
DMSUnknownEntity,
|
|
33
|
+
EdgeEntity,
|
|
24
34
|
EntityTypes,
|
|
25
35
|
MultiValueTypeInfo,
|
|
26
36
|
ReferenceEntity,
|
|
27
37
|
RelationshipEntity,
|
|
38
|
+
ReverseConnectionEntity,
|
|
28
39
|
UnknownEntity,
|
|
29
40
|
ViewEntity,
|
|
30
|
-
ViewPropertyEntity,
|
|
31
41
|
)
|
|
42
|
+
from cognite.neat.rules.models.information import InformationClass, InformationMetadata, InformationProperty
|
|
43
|
+
|
|
44
|
+
from ._base import RulesTransformer
|
|
45
|
+
|
|
46
|
+
T_VerifiedInRules = TypeVar("T_VerifiedInRules", bound=VerifiedRules)
|
|
47
|
+
T_VerifiedOutRules = TypeVar("T_VerifiedOutRules", bound=VerifiedRules)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConversionTransformer(RulesTransformer[T_VerifiedInRules, T_VerifiedOutRules], ABC):
|
|
51
|
+
"""Base class for all conversion transformers."""
|
|
52
|
+
|
|
53
|
+
def transform(self, rules: T_VerifiedInRules | OutRules[T_VerifiedInRules]) -> JustRules[T_VerifiedOutRules]:
|
|
54
|
+
out = self._transform(self._to_rules(rules))
|
|
55
|
+
return JustRules(out)
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def _transform(self, rules: T_VerifiedInRules) -> T_VerifiedOutRules:
|
|
59
|
+
raise NotImplementedError()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class InformationToDMS(ConversionTransformer[InformationRules, DMSRules]):
|
|
63
|
+
"""Converts InformationRules to DMSRules."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, ignore_undefined_value_types: bool = False):
|
|
66
|
+
self.ignore_undefined_value_types = ignore_undefined_value_types
|
|
67
|
+
|
|
68
|
+
def _transform(self, rules: InformationRules) -> DMSRules:
|
|
69
|
+
return _InformationRulesConverter(rules).as_dms_rules(self.ignore_undefined_value_types)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class InformationToAsset(ConversionTransformer[InformationRules, AssetRules]):
|
|
73
|
+
"""Converts InformationRules to AssetRules."""
|
|
74
|
+
|
|
75
|
+
def _transform(self, rules: InformationRules) -> AssetRules:
|
|
76
|
+
return _InformationRulesConverter(rules).as_asset_architect_rules()
|
|
77
|
+
|
|
32
78
|
|
|
33
|
-
|
|
79
|
+
class AssetToInformation(ConversionTransformer[AssetRules, InformationRules]):
|
|
80
|
+
"""Converts AssetRules to InformationRules."""
|
|
34
81
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
82
|
+
def _transform(self, rules: AssetRules) -> InformationRules:
|
|
83
|
+
return InformationRules.model_validate(rules.model_dump())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DMSToInformation(ConversionTransformer[DMSRules, InformationRules]):
|
|
87
|
+
"""Converts DMSRules to InformationRules."""
|
|
88
|
+
|
|
89
|
+
def _transform(self, rules: DMSRules) -> InformationRules:
|
|
90
|
+
return _DMSRulesConverter(rules).as_information_rules()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ConvertToRules(ConversionTransformer[VerifiedRules, VerifiedRules]):
|
|
94
|
+
"""Converts any rules to any rules."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, out_cls: type[VerifiedRules]):
|
|
97
|
+
self._out_cls = out_cls
|
|
98
|
+
|
|
99
|
+
def _transform(self, rules: VerifiedRules) -> VerifiedRules:
|
|
100
|
+
if isinstance(rules, self._out_cls):
|
|
101
|
+
return rules
|
|
102
|
+
if isinstance(rules, InformationRules) and self._out_cls is DMSRules:
|
|
103
|
+
return InformationToDMS().transform(rules).rules
|
|
104
|
+
if isinstance(rules, InformationRules) and self._out_cls is AssetRules:
|
|
105
|
+
return InformationToAsset().transform(rules).rules
|
|
106
|
+
if isinstance(rules, AssetRules) and self._out_cls is InformationRules:
|
|
107
|
+
return AssetToInformation().transform(rules).rules
|
|
108
|
+
if isinstance(rules, AssetRules) and self._out_cls is DMSRules:
|
|
109
|
+
return InformationToDMS().transform(AssetToInformation().transform(rules)).rules
|
|
110
|
+
if isinstance(rules, DMSRules) and self._out_cls is InformationRules:
|
|
111
|
+
return DMSToInformation().transform(rules).rules
|
|
112
|
+
if isinstance(rules, DMSRules) and self._out_cls is AssetRules:
|
|
113
|
+
return InformationToAsset().transform(DMSToInformation().transform(rules)).rules
|
|
114
|
+
raise ValueError(f"Unsupported conversion from {type(rules)} to {self._out_cls}")
|
|
38
115
|
|
|
39
116
|
|
|
40
117
|
class _InformationRulesConverter:
|
|
@@ -79,7 +156,7 @@ class _InformationRulesConverter:
|
|
|
79
156
|
prefixes=self.rules.prefixes,
|
|
80
157
|
)
|
|
81
158
|
|
|
82
|
-
def as_dms_rules(self) -> "DMSRules":
|
|
159
|
+
def as_dms_rules(self, ignore_undefined_value_types: bool = False) -> "DMSRules":
|
|
83
160
|
from cognite.neat.rules.models.dms._rules import (
|
|
84
161
|
DMSContainer,
|
|
85
162
|
DMSProperty,
|
|
@@ -95,6 +172,8 @@ class _InformationRulesConverter:
|
|
|
95
172
|
properties_by_class: dict[ClassEntity, list[DMSProperty]] = defaultdict(list)
|
|
96
173
|
referenced_containers: dict[ContainerEntity, Counter[ClassEntity]] = defaultdict(Counter)
|
|
97
174
|
for prop in self.rules.properties:
|
|
175
|
+
if ignore_undefined_value_types and isinstance(prop.value_type, UnknownEntity):
|
|
176
|
+
continue
|
|
98
177
|
dms_property = self._as_dms_property(prop, default_space, default_version)
|
|
99
178
|
properties_by_class[prop.class_].append(dms_property)
|
|
100
179
|
if dms_property.container:
|
|
@@ -112,8 +191,10 @@ class _InformationRulesConverter:
|
|
|
112
191
|
for cls_ in self.rules.classes
|
|
113
192
|
]
|
|
114
193
|
|
|
115
|
-
last_dms_rules = self.rules.last.as_dms_rules() if self.rules.last else None
|
|
116
|
-
ref_dms_rules =
|
|
194
|
+
last_dms_rules = _InformationRulesConverter(self.rules.last).as_dms_rules() if self.rules.last else None
|
|
195
|
+
ref_dms_rules = (
|
|
196
|
+
_InformationRulesConverter(self.rules.reference).as_dms_rules() if self.rules.reference else None
|
|
197
|
+
)
|
|
117
198
|
|
|
118
199
|
class_by_entity = {cls_.class_: cls_ for cls_ in self.rules.classes}
|
|
119
200
|
if self.rules.last:
|
|
@@ -197,7 +278,7 @@ class _InformationRulesConverter:
|
|
|
197
278
|
from cognite.neat.rules.models.dms._rules import DMSProperty
|
|
198
279
|
|
|
199
280
|
# returns property type, which can be ObjectProperty or DatatypeProperty
|
|
200
|
-
value_type: DataType | ViewEntity |
|
|
281
|
+
value_type: DataType | ViewEntity | DMSUnknownEntity
|
|
201
282
|
if isinstance(prop.value_type, DataType):
|
|
202
283
|
value_type = prop.value_type
|
|
203
284
|
elif isinstance(prop.value_type, UnknownEntity):
|
|
@@ -209,17 +290,22 @@ class _InformationRulesConverter:
|
|
|
209
290
|
else:
|
|
210
291
|
raise ValueError(f"Unsupported value type: {prop.value_type.type_}")
|
|
211
292
|
|
|
212
|
-
|
|
213
|
-
if isinstance(value_type, ViewEntity
|
|
214
|
-
|
|
293
|
+
connection: Literal["direct"] | ReverseConnectionEntity | EdgeEntity | None = None
|
|
294
|
+
if isinstance(value_type, ViewEntity):
|
|
295
|
+
# Default connection type.
|
|
296
|
+
connection = EdgeEntity() if prop.is_list else "direct"
|
|
297
|
+
|
|
298
|
+
# defaulting to direct connection
|
|
299
|
+
elif isinstance(value_type, DMSUnknownEntity):
|
|
300
|
+
connection = "direct"
|
|
215
301
|
|
|
216
302
|
container: ContainerEntity | None = None
|
|
217
303
|
container_property: str | None = None
|
|
218
304
|
is_list: bool | None = prop.is_list
|
|
219
305
|
nullable: bool | None = not prop.is_mandatory
|
|
220
|
-
if
|
|
306
|
+
if isinstance(connection, EdgeEntity):
|
|
221
307
|
nullable = None
|
|
222
|
-
elif
|
|
308
|
+
elif connection == "direct":
|
|
223
309
|
nullable = True
|
|
224
310
|
container, container_property = self._get_container(prop, default_space)
|
|
225
311
|
else:
|
|
@@ -232,7 +318,7 @@ class _InformationRulesConverter:
|
|
|
232
318
|
value_type=value_type,
|
|
233
319
|
nullable=nullable,
|
|
234
320
|
is_list=is_list,
|
|
235
|
-
connection=
|
|
321
|
+
connection=connection,
|
|
236
322
|
default=prop.default,
|
|
237
323
|
reference=prop.reference,
|
|
238
324
|
container=container,
|
|
@@ -326,3 +412,122 @@ class _InformationRulesConverter:
|
|
|
326
412
|
return data_types.DateTime()
|
|
327
413
|
|
|
328
414
|
return data_types.String()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class _DMSRulesConverter:
|
|
418
|
+
def __init__(self, dms: DMSRules):
|
|
419
|
+
self.dms = dms
|
|
420
|
+
|
|
421
|
+
def as_domain_rules(self) -> "DomainRules":
|
|
422
|
+
raise NotImplementedError("DomainRules not implemented yet")
|
|
423
|
+
|
|
424
|
+
def as_information_rules(
|
|
425
|
+
self,
|
|
426
|
+
) -> "InformationRules":
|
|
427
|
+
from cognite.neat.rules.models.information._rules import (
|
|
428
|
+
InformationClass,
|
|
429
|
+
InformationProperty,
|
|
430
|
+
InformationRules,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
dms = self.dms.metadata
|
|
434
|
+
|
|
435
|
+
metadata = self._convert_metadata_to_info(dms)
|
|
436
|
+
|
|
437
|
+
classes = [
|
|
438
|
+
InformationClass(
|
|
439
|
+
# we do not want a version in class as we use URI for the class
|
|
440
|
+
class_=ClassEntity(prefix=view.class_.prefix, suffix=view.class_.suffix),
|
|
441
|
+
description=view.description,
|
|
442
|
+
parent=[
|
|
443
|
+
# we do not want a version in class as we use URI for the class
|
|
444
|
+
implemented_view.as_class(skip_version=True)
|
|
445
|
+
# We only want parents in the same namespace, parent in a different namespace is a reference
|
|
446
|
+
for implemented_view in view.implements or []
|
|
447
|
+
if implemented_view.prefix == view.class_.prefix
|
|
448
|
+
],
|
|
449
|
+
reference=self._get_class_reference(view),
|
|
450
|
+
)
|
|
451
|
+
for view in self.dms.views
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
properties: list[InformationProperty] = []
|
|
455
|
+
value_type: DataType | ClassEntity | str
|
|
456
|
+
for property_ in self.dms.properties:
|
|
457
|
+
if isinstance(property_.value_type, DataType):
|
|
458
|
+
value_type = property_.value_type
|
|
459
|
+
elif isinstance(property_.value_type, ViewEntity):
|
|
460
|
+
value_type = ClassEntity(
|
|
461
|
+
prefix=property_.value_type.prefix,
|
|
462
|
+
suffix=property_.value_type.suffix,
|
|
463
|
+
)
|
|
464
|
+
elif isinstance(property_.value_type, DMSUnknownEntity):
|
|
465
|
+
value_type = UnknownEntity()
|
|
466
|
+
else:
|
|
467
|
+
raise ValueError(f"Unsupported value type: {property_.value_type.type_}")
|
|
468
|
+
|
|
469
|
+
properties.append(
|
|
470
|
+
InformationProperty(
|
|
471
|
+
# Removing version
|
|
472
|
+
class_=ClassEntity(suffix=property_.class_.suffix, prefix=property_.class_.prefix),
|
|
473
|
+
property_=property_.view_property,
|
|
474
|
+
value_type=value_type,
|
|
475
|
+
description=property_.description,
|
|
476
|
+
min_count=0 if property_.nullable or property_.nullable is None else 1,
|
|
477
|
+
max_count=float("inf") if property_.is_list or property_.nullable is None else 1,
|
|
478
|
+
reference=self._get_property_reference(property_),
|
|
479
|
+
)
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
return InformationRules(
|
|
483
|
+
metadata=metadata,
|
|
484
|
+
properties=SheetList[InformationProperty](data=properties),
|
|
485
|
+
classes=SheetList[InformationClass](data=classes),
|
|
486
|
+
last=_DMSRulesConverter(self.dms.last).as_information_rules() if self.dms.last else None,
|
|
487
|
+
reference=_DMSRulesConverter(self.dms.reference).as_information_rules() if self.dms.reference else None,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
@classmethod
|
|
491
|
+
def _convert_metadata_to_info(cls, metadata: DMSMetadata) -> "InformationMetadata":
|
|
492
|
+
from cognite.neat.rules.models.information._rules import InformationMetadata
|
|
493
|
+
|
|
494
|
+
prefix = metadata.space
|
|
495
|
+
return InformationMetadata(
|
|
496
|
+
schema_=metadata.schema_,
|
|
497
|
+
data_model_type=metadata.data_model_type,
|
|
498
|
+
extension=metadata.extension,
|
|
499
|
+
prefix=prefix,
|
|
500
|
+
namespace=Namespace(f"https://purl.orgl/neat/{prefix}/"),
|
|
501
|
+
version=metadata.version,
|
|
502
|
+
description=metadata.description,
|
|
503
|
+
name=metadata.name or metadata.external_id,
|
|
504
|
+
creator=metadata.creator,
|
|
505
|
+
created=metadata.created,
|
|
506
|
+
updated=metadata.updated,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
@classmethod
|
|
510
|
+
def _get_class_reference(cls, view: DMSView) -> ReferenceEntity | None:
|
|
511
|
+
parents_other_namespace = [parent for parent in view.implements or [] if parent.prefix != view.class_.prefix]
|
|
512
|
+
if len(parents_other_namespace) == 0:
|
|
513
|
+
return None
|
|
514
|
+
if len(parents_other_namespace) > 1:
|
|
515
|
+
warnings.warn(
|
|
516
|
+
ParentInDifferentSpaceWarning(view.view.as_id()),
|
|
517
|
+
stacklevel=2,
|
|
518
|
+
)
|
|
519
|
+
other_parent = parents_other_namespace[0]
|
|
520
|
+
|
|
521
|
+
return ReferenceEntity(prefix=other_parent.prefix, suffix=other_parent.suffix)
|
|
522
|
+
|
|
523
|
+
@classmethod
|
|
524
|
+
def _get_property_reference(cls, property_: DMSProperty) -> ReferenceEntity | None:
|
|
525
|
+
has_container_other_namespace = property_.container and property_.container.prefix != property_.class_.prefix
|
|
526
|
+
if not has_container_other_namespace:
|
|
527
|
+
return None
|
|
528
|
+
container = cast(ContainerEntity, property_.container)
|
|
529
|
+
return ReferenceEntity(
|
|
530
|
+
prefix=container.prefix,
|
|
531
|
+
suffix=container.suffix,
|
|
532
|
+
property=property_.container_property,
|
|
533
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
|
|
4
|
+
from cognite.neat.rules._shared import JustRules, OutRules
|
|
5
|
+
from cognite.neat.rules.models import DMSRules
|
|
6
|
+
from cognite.neat.rules.models.dms import DMSProperty
|
|
7
|
+
from cognite.neat.rules.models.entities import ReferenceEntity
|
|
8
|
+
|
|
9
|
+
from ._base import RulesTransformer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MapOntoTransformers(RulesTransformer[DMSRules, DMSRules], ABC):
|
|
13
|
+
"""Base class for transformers that map one rule onto another."""
|
|
14
|
+
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MapOneToOne(MapOntoTransformers):
|
|
19
|
+
"""Takes transform data models and makes it into an extension of the reference data model.
|
|
20
|
+
|
|
21
|
+
Note this transformer mutates the input rules.
|
|
22
|
+
|
|
23
|
+
The argument view_extension_mapping is a dictionary where the keys are views of this data model,
|
|
24
|
+
and each value is the view of the reference data model that the view should extend. For example:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
view_extension_mapping = {"Pump": "Asset"}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This would make the view "Pump" in this data model extend the view "Asset" in the reference data model.
|
|
31
|
+
Note that all the keys in the dictionary must be external ids of views in this data model,
|
|
32
|
+
and all the values must be external ids of views in the reference data model.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
reference: The reference data model
|
|
36
|
+
view_extension_mapping: A dictionary mapping views in this data model to views in the reference data model
|
|
37
|
+
default_extension: The default view in the reference data model that views in this
|
|
38
|
+
data model should extend if no mapping is provided.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self, reference: DMSRules, view_extension_mapping: dict[str, str], default_extension: str | None = None
|
|
44
|
+
) -> None:
|
|
45
|
+
self.reference = reference
|
|
46
|
+
self.view_extension_mapping = view_extension_mapping
|
|
47
|
+
self.default_extension = default_extension
|
|
48
|
+
|
|
49
|
+
def transform(self, rules: DMSRules | OutRules[DMSRules]) -> JustRules[DMSRules]:
|
|
50
|
+
solution: DMSRules = self._to_rules(rules)
|
|
51
|
+
if solution.reference is not None:
|
|
52
|
+
raise ValueError("Reference already exists")
|
|
53
|
+
solution.reference = self.reference
|
|
54
|
+
view_by_external_id = {view.view.external_id: view for view in solution.views}
|
|
55
|
+
ref_view_by_external_id = {view.view.external_id: view for view in self.reference.views}
|
|
56
|
+
|
|
57
|
+
if invalid_views := set(self.view_extension_mapping.keys()) - set(view_by_external_id.keys()):
|
|
58
|
+
raise ValueError(f"Views are not in this dat model {invalid_views}")
|
|
59
|
+
if invalid_views := set(self.view_extension_mapping.values()) - set(ref_view_by_external_id.keys()):
|
|
60
|
+
raise ValueError(f"Views are not in the reference data model {invalid_views}")
|
|
61
|
+
if self.default_extension and self.default_extension not in ref_view_by_external_id:
|
|
62
|
+
raise ValueError(f"Default extension view not in the reference data model {self.default_extension}")
|
|
63
|
+
|
|
64
|
+
properties_by_view_external_id: dict[str, dict[str, DMSProperty]] = defaultdict(dict)
|
|
65
|
+
for prop in solution.properties:
|
|
66
|
+
properties_by_view_external_id[prop.view.external_id][prop.view_property] = prop
|
|
67
|
+
|
|
68
|
+
ref_properties_by_view_external_id: dict[str, dict[str, DMSProperty]] = defaultdict(dict)
|
|
69
|
+
for prop in self.reference.properties:
|
|
70
|
+
ref_properties_by_view_external_id[prop.view.external_id][prop.view_property] = prop
|
|
71
|
+
|
|
72
|
+
for view_external_id, view in view_by_external_id.items():
|
|
73
|
+
if view_external_id in self.view_extension_mapping:
|
|
74
|
+
ref_external_id = self.view_extension_mapping[view_external_id]
|
|
75
|
+
elif self.default_extension:
|
|
76
|
+
ref_external_id = self.default_extension
|
|
77
|
+
else:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
ref_view = ref_view_by_external_id[ref_external_id]
|
|
81
|
+
shared_properties = set(properties_by_view_external_id[view_external_id].keys()) & set(
|
|
82
|
+
ref_properties_by_view_external_id[ref_external_id].keys()
|
|
83
|
+
)
|
|
84
|
+
if shared_properties:
|
|
85
|
+
if view.implements is None:
|
|
86
|
+
view.implements = [ref_view.view]
|
|
87
|
+
elif isinstance(view.implements, list) and ref_view.view not in view.implements:
|
|
88
|
+
view.implements.append(ref_view.view)
|
|
89
|
+
for prop_name in shared_properties:
|
|
90
|
+
prop = properties_by_view_external_id[view_external_id][prop_name]
|
|
91
|
+
ref_prop = ref_properties_by_view_external_id[ref_external_id][prop_name]
|
|
92
|
+
if ref_prop.container and ref_prop.container_property:
|
|
93
|
+
prop.container = ref_prop.container
|
|
94
|
+
prop.container_property = ref_prop.container_property
|
|
95
|
+
prop.reference = ReferenceEntity.from_entity(ref_prop.view, ref_prop.view_property)
|
|
96
|
+
|
|
97
|
+
return JustRules(solution)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
|
|
3
|
+
from cognite.neat.issues.errors import NeatValueError
|
|
4
|
+
from cognite.neat.rules._shared import InputRules, MaybeRules, VerifiedRules
|
|
5
|
+
from cognite.neat.rules.importers import BaseImporter
|
|
6
|
+
from cognite.neat.rules.models import VERIFIED_RULES_BY_ROLE, RoleTypes
|
|
7
|
+
|
|
8
|
+
from ._base import RulesPipeline, RulesTransformer
|
|
9
|
+
from ._converters import ConvertToRules
|
|
10
|
+
from ._verification import VerifyAnyRules
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ImporterPipeline(RulesPipeline[InputRules, VerifiedRules]):
|
|
14
|
+
"""This is a standard pipeline that verifies, convert and return the rules from the importer."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
importer: BaseImporter[InputRules],
|
|
19
|
+
items: Iterable[RulesTransformer[InputRules, VerifiedRules]] | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__(items or [])
|
|
22
|
+
self._importer = importer
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _create_pipeline(cls, importer: BaseImporter[InputRules], role: RoleTypes | None = None) -> "ImporterPipeline":
|
|
26
|
+
items: list[RulesTransformer] = [VerifyAnyRules(errors="continue")]
|
|
27
|
+
if role is not None:
|
|
28
|
+
out_cls = VERIFIED_RULES_BY_ROLE[role]
|
|
29
|
+
items.append(ConvertToRules(out_cls))
|
|
30
|
+
return cls(importer, items)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def try_verify(cls, importer: BaseImporter, role: RoleTypes | None = None) -> MaybeRules[VerifiedRules]:
|
|
34
|
+
"""This is a standard pipeline that verifies, convert and return the rules from the importer.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
importer: The importer to use.
|
|
38
|
+
role: The type of rules to convert to. If None, the rules will not be converted.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The verified rules.
|
|
42
|
+
"""
|
|
43
|
+
return cls._create_pipeline(importer, role).try_execute()
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def verify(cls, importer: BaseImporter, role: RoleTypes | None = None) -> VerifiedRules:
|
|
47
|
+
"""Verify the rules."""
|
|
48
|
+
return cls._create_pipeline(importer, role).execute()
|
|
49
|
+
|
|
50
|
+
def try_execute(self) -> MaybeRules[VerifiedRules]:
|
|
51
|
+
"""Try to execute the pipeline from importer to rules."""
|
|
52
|
+
rules = self._importer.to_rules()
|
|
53
|
+
return self.try_transform(rules)
|
|
54
|
+
|
|
55
|
+
def execute(self) -> VerifiedRules:
|
|
56
|
+
"""Execute the pipeline from importer to rules."""
|
|
57
|
+
rules = self._importer.to_rules()
|
|
58
|
+
out = self.transform(rules).get_rules()
|
|
59
|
+
if out is None:
|
|
60
|
+
raise NeatValueError("Failed to convert rules")
|
|
61
|
+
return out
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from cognite.neat.issues import IssueList, NeatError, NeatWarning
|
|
10
|
+
from cognite.neat.issues.errors import NeatTypeError
|
|
11
|
+
from cognite.neat.rules._shared import (
|
|
12
|
+
InputRules,
|
|
13
|
+
MaybeRules,
|
|
14
|
+
OutRules,
|
|
15
|
+
ReadRules,
|
|
16
|
+
T_InputRules,
|
|
17
|
+
T_VerifiedRules,
|
|
18
|
+
VerifiedRules,
|
|
19
|
+
)
|
|
20
|
+
from cognite.neat.rules.models import (
|
|
21
|
+
AssetInputRules,
|
|
22
|
+
AssetRules,
|
|
23
|
+
DMSInputRules,
|
|
24
|
+
DMSRules,
|
|
25
|
+
DomainInputRules,
|
|
26
|
+
DomainRules,
|
|
27
|
+
InformationInputRules,
|
|
28
|
+
InformationRules,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from ._base import RulesTransformer
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VerificationTransformer(RulesTransformer[T_InputRules, T_VerifiedRules], ABC):
|
|
35
|
+
"""Base class for all verification transformers."""
|
|
36
|
+
|
|
37
|
+
_rules_cls: type[T_VerifiedRules]
|
|
38
|
+
|
|
39
|
+
def __init__(self, errors: Literal["raise", "continue"]) -> None:
|
|
40
|
+
self.errors = errors
|
|
41
|
+
|
|
42
|
+
def transform(self, rules: T_InputRules | OutRules[T_InputRules]) -> MaybeRules[T_VerifiedRules]:
|
|
43
|
+
issues = IssueList()
|
|
44
|
+
in_: T_InputRules = self._to_rules(rules)
|
|
45
|
+
error_args: dict[str, Any] = {}
|
|
46
|
+
if isinstance(rules, ReadRules):
|
|
47
|
+
error_args = rules.read_context
|
|
48
|
+
verified_rules: T_VerifiedRules | None = None
|
|
49
|
+
with _handle_issues(issues, NeatError, NeatWarning, error_args) as future:
|
|
50
|
+
rules_cls = self._get_rules_cls(in_)
|
|
51
|
+
verified_rules = rules_cls.model_validate(in_.dump()) # type: ignore[assignment]
|
|
52
|
+
|
|
53
|
+
if (future.result == "failure" or issues.has_errors or verified_rules is None) and self.errors == "raise":
|
|
54
|
+
raise issues.as_errors()
|
|
55
|
+
return MaybeRules[T_VerifiedRules](
|
|
56
|
+
rules=verified_rules,
|
|
57
|
+
issues=issues,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _get_rules_cls(self, in_: T_InputRules) -> type[T_VerifiedRules]:
|
|
61
|
+
return self._rules_cls
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class VerifyDMSRules(VerificationTransformer[DMSInputRules, DMSRules]):
|
|
65
|
+
"""Class to verify DMS rules."""
|
|
66
|
+
|
|
67
|
+
_rules_cls = DMSRules
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class VerifyInformationRules(VerificationTransformer[InformationInputRules, InformationRules]):
|
|
71
|
+
"""Class to verify Information rules."""
|
|
72
|
+
|
|
73
|
+
_rules_cls = InformationRules
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class VerifyAssetRules(VerificationTransformer[AssetInputRules, AssetRules]):
|
|
77
|
+
"""Class to verify Asset rules."""
|
|
78
|
+
|
|
79
|
+
_rules_cls = AssetRules
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class VerifyAnyRules(VerificationTransformer[InputRules, VerifiedRules]):
|
|
83
|
+
"""Class to verify arbitrary rules"""
|
|
84
|
+
|
|
85
|
+
def _get_rules_cls(self, in_: InputRules) -> type[VerifiedRules]:
|
|
86
|
+
if isinstance(in_, InformationInputRules):
|
|
87
|
+
return InformationRules
|
|
88
|
+
elif isinstance(in_, DMSInputRules):
|
|
89
|
+
return DMSRules
|
|
90
|
+
elif isinstance(in_, AssetInputRules):
|
|
91
|
+
return AssetRules
|
|
92
|
+
elif isinstance(in_, DomainInputRules):
|
|
93
|
+
return DomainRules
|
|
94
|
+
else:
|
|
95
|
+
raise NeatTypeError(f"Unsupported rules type: {type(in_)}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _FutureResult:
|
|
99
|
+
def __init__(self) -> None:
|
|
100
|
+
self._result: Literal["success", "failure", "pending"] = "pending"
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def result(self) -> Literal["success", "failure", "pending"]:
|
|
104
|
+
return self._result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@contextmanager
|
|
108
|
+
def _handle_issues(
|
|
109
|
+
issues: IssueList,
|
|
110
|
+
error_cls: type[NeatError] = NeatError,
|
|
111
|
+
warning_cls: type[NeatWarning] = NeatWarning,
|
|
112
|
+
error_args: dict[str, Any] | None = None,
|
|
113
|
+
) -> Iterator[_FutureResult]:
|
|
114
|
+
"""This is an internal help function to handle issues and warnings.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
issues: The issues list to append to.
|
|
118
|
+
error_cls: The class used to convert errors to issues.
|
|
119
|
+
warning_cls: The class used to convert warnings to issues.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
FutureResult: A future result object that can be used to check the result of the context manager.
|
|
123
|
+
"""
|
|
124
|
+
with warnings.catch_warnings(record=True) as warning_logger:
|
|
125
|
+
warnings.simplefilter("always")
|
|
126
|
+
future_result = _FutureResult()
|
|
127
|
+
try:
|
|
128
|
+
yield future_result
|
|
129
|
+
except ValidationError as e:
|
|
130
|
+
issues.extend(error_cls.from_pydantic_errors(e.errors(), **(error_args or {})))
|
|
131
|
+
future_result._result = "failure"
|
|
132
|
+
else:
|
|
133
|
+
future_result._result = "success"
|
|
134
|
+
finally:
|
|
135
|
+
if warning_logger:
|
|
136
|
+
issues.extend([warning_cls.from_warning(warning) for warning in warning_logger]) # type: ignore[misc]
|
cognite/neat/store/_base.py
CHANGED
|
@@ -12,7 +12,7 @@ from rdflib.plugins.stores.sparqlstore import SPARQLUpdateStore
|
|
|
12
12
|
from cognite.neat.constants import DEFAULT_NAMESPACE
|
|
13
13
|
from cognite.neat.graph._shared import MIMETypes
|
|
14
14
|
from cognite.neat.graph.extractors import RdfFileExtractor, TripleExtractors
|
|
15
|
-
from cognite.neat.graph.models import Triple
|
|
15
|
+
from cognite.neat.graph.models import InstanceType, Triple
|
|
16
16
|
from cognite.neat.graph.queries import Queries
|
|
17
17
|
from cognite.neat.graph.transformers import Transformers
|
|
18
18
|
from cognite.neat.rules.analysis import InformationAnalysis
|
|
@@ -173,7 +173,7 @@ class NeatGraphStore:
|
|
|
173
173
|
)
|
|
174
174
|
)
|
|
175
175
|
|
|
176
|
-
def read(self, class_: str) -> Iterable[tuple[str, dict[str, list[str]]]]:
|
|
176
|
+
def read(self, class_: str) -> Iterable[tuple[str, dict[str | InstanceType, list[str]]]]:
|
|
177
177
|
"""Read instances for given view from the graph store."""
|
|
178
178
|
|
|
179
179
|
if not self.rules:
|
|
@@ -69,6 +69,10 @@ class Change(FrozenNeatObject):
|
|
|
69
69
|
activity: Activity
|
|
70
70
|
entity: Entity
|
|
71
71
|
description: str
|
|
72
|
+
# triples that were added to the graph store
|
|
73
|
+
addition: list[tuple[URIRef, URIRef, URIRef | Literal]] | None = None
|
|
74
|
+
# triples that were removed from the graph store
|
|
75
|
+
subtraction: list[tuple[URIRef, URIRef, URIRef | Literal]] | None = None
|
|
72
76
|
|
|
73
77
|
def as_triples(self):
|
|
74
78
|
return self.agent.as_triples() + self.activity.as_triples() + self.entity.as_triples()
|
|
@@ -77,7 +81,12 @@ class Change(FrozenNeatObject):
|
|
|
77
81
|
def record(cls, activity: str, start: datetime, end: datetime, description: str):
|
|
78
82
|
"""User friendly method to record a change that occurred in the graph store."""
|
|
79
83
|
agent = Agent()
|
|
80
|
-
activity = Activity(
|
|
84
|
+
activity = Activity(
|
|
85
|
+
used=activity,
|
|
86
|
+
was_associated_with=agent,
|
|
87
|
+
started_at_time=start,
|
|
88
|
+
ended_at_time=end,
|
|
89
|
+
)
|
|
81
90
|
entity = Entity(was_generated_by=activity, was_attributed_to=agent)
|
|
82
91
|
return cls(agent, activity, entity, description)
|
|
83
92
|
|