cognite-neat 0.109.4__py3-none-any.whl → 0.111.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/_alpha.py +8 -0
- cognite/neat/_client/_api/schema.py +43 -1
- cognite/neat/_client/data_classes/schema.py +4 -4
- cognite/neat/_constants.py +15 -1
- cognite/neat/_graph/extractors/__init__.py +4 -0
- cognite/neat/_graph/extractors/_classic_cdf/_base.py +8 -16
- cognite/neat/_graph/extractors/_classic_cdf/_classic.py +48 -19
- cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +23 -17
- cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +15 -17
- cognite/neat/_graph/extractors/_dict.py +102 -0
- cognite/neat/_graph/extractors/_dms.py +27 -40
- cognite/neat/_graph/extractors/_dms_graph.py +30 -3
- cognite/neat/_graph/extractors/_iodd.py +3 -3
- cognite/neat/_graph/extractors/_mock_graph_generator.py +9 -7
- cognite/neat/_graph/extractors/_raw.py +67 -0
- cognite/neat/_graph/loaders/_base.py +20 -4
- cognite/neat/_graph/loaders/_rdf2dms.py +476 -383
- cognite/neat/_graph/queries/_base.py +163 -133
- cognite/neat/_graph/transformers/__init__.py +1 -3
- cognite/neat/_graph/transformers/_classic_cdf.py +6 -22
- cognite/neat/_graph/transformers/_rdfpath.py +2 -49
- cognite/neat/_issues/__init__.py +1 -6
- cognite/neat/_issues/_base.py +21 -252
- cognite/neat/_issues/_contextmanagers.py +46 -0
- cognite/neat/_issues/_factory.py +69 -0
- cognite/neat/_issues/errors/__init__.py +20 -4
- cognite/neat/_issues/errors/_external.py +7 -0
- cognite/neat/_issues/errors/_wrapper.py +81 -3
- cognite/neat/_issues/formatters.py +4 -4
- cognite/neat/_issues/warnings/__init__.py +3 -2
- cognite/neat/_issues/warnings/_properties.py +8 -0
- cognite/neat/_issues/warnings/user_modeling.py +12 -0
- cognite/neat/_rules/_constants.py +12 -0
- cognite/neat/_rules/_shared.py +3 -2
- cognite/neat/_rules/analysis/__init__.py +2 -3
- cognite/neat/_rules/analysis/_base.py +430 -259
- cognite/neat/_rules/catalog/info-rules-imf.xlsx +0 -0
- cognite/neat/_rules/exporters/_rules2excel.py +3 -9
- cognite/neat/_rules/exporters/_rules2instance_template.py +2 -2
- cognite/neat/_rules/exporters/_rules2ontology.py +5 -4
- cognite/neat/_rules/importers/_base.py +2 -47
- cognite/neat/_rules/importers/_dms2rules.py +7 -10
- cognite/neat/_rules/importers/_dtdl2rules/dtdl_importer.py +2 -2
- cognite/neat/_rules/importers/_rdf/_inference2rules.py +66 -26
- cognite/neat/_rules/importers/_rdf/_shared.py +1 -1
- cognite/neat/_rules/importers/_spreadsheet2rules.py +12 -9
- cognite/neat/_rules/models/_base_rules.py +0 -2
- cognite/neat/_rules/models/data_types.py +7 -0
- cognite/neat/_rules/models/dms/_exporter.py +9 -8
- cognite/neat/_rules/models/dms/_rules.py +29 -2
- cognite/neat/_rules/models/dms/_rules_input.py +9 -1
- cognite/neat/_rules/models/dms/_validation.py +115 -5
- cognite/neat/_rules/models/entities/_loaders.py +1 -1
- cognite/neat/_rules/models/entities/_multi_value.py +2 -2
- cognite/neat/_rules/models/entities/_single_value.py +8 -3
- cognite/neat/_rules/models/entities/_wrapped.py +2 -2
- cognite/neat/_rules/models/information/_rules.py +18 -17
- cognite/neat/_rules/models/information/_rules_input.py +3 -1
- cognite/neat/_rules/models/information/_validation.py +66 -17
- cognite/neat/_rules/transformers/__init__.py +8 -2
- cognite/neat/_rules/transformers/_converters.py +234 -44
- cognite/neat/_rules/transformers/_verification.py +5 -10
- cognite/neat/_session/_base.py +6 -4
- cognite/neat/_session/_explore.py +39 -0
- cognite/neat/_session/_inspect.py +25 -6
- cognite/neat/_session/_prepare.py +12 -0
- cognite/neat/_session/_read.py +88 -20
- cognite/neat/_session/_set.py +7 -1
- cognite/neat/_session/_show.py +11 -123
- cognite/neat/_session/_state.py +6 -2
- cognite/neat/_session/_subset.py +64 -0
- cognite/neat/_session/_to.py +177 -19
- cognite/neat/_store/_graph_store.py +9 -246
- cognite/neat/_utils/rdf_.py +36 -5
- cognite/neat/_utils/spreadsheet.py +44 -1
- cognite/neat/_utils/text.py +124 -37
- cognite/neat/_utils/upload.py +2 -0
- cognite/neat/_version.py +2 -2
- {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/RECORD +83 -82
- {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/WHEEL +1 -1
- cognite/neat/_graph/queries/_construct.py +0 -187
- cognite/neat/_graph/queries/_shared.py +0 -173
- cognite/neat/_rules/analysis/_dms.py +0 -57
- cognite/neat/_rules/analysis/_information.py +0 -249
- cognite/neat/_rules/models/_rdfpath.py +0 -372
- {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/entry_points.txt +0 -0
|
@@ -6,11 +6,12 @@ from collections import Counter, defaultdict
|
|
|
6
6
|
from collections.abc import Collection, Mapping
|
|
7
7
|
from datetime import date, datetime
|
|
8
8
|
from functools import cached_property
|
|
9
|
-
from typing import ClassVar, Literal, TypeVar, cast, overload
|
|
9
|
+
from typing import Any, ClassVar, Literal, TypeVar, cast, overload
|
|
10
10
|
|
|
11
11
|
from cognite.client.data_classes import data_modeling as dms
|
|
12
12
|
from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier, ViewId
|
|
13
13
|
from cognite.client.utils.useful_types import SequenceNotStr
|
|
14
|
+
from pydantic import ValidationError
|
|
14
15
|
from rdflib import Namespace
|
|
15
16
|
|
|
16
17
|
from cognite.neat._client import NeatClient
|
|
@@ -33,7 +34,7 @@ from cognite.neat._rules._shared import (
|
|
|
33
34
|
ReadRules,
|
|
34
35
|
VerifiedRules,
|
|
35
36
|
)
|
|
36
|
-
from cognite.neat._rules.analysis import
|
|
37
|
+
from cognite.neat._rules.analysis import RulesAnalysis
|
|
37
38
|
from cognite.neat._rules.importers import DMSImporter
|
|
38
39
|
from cognite.neat._rules.models import (
|
|
39
40
|
DMSInputRules,
|
|
@@ -43,8 +44,6 @@ from cognite.neat._rules.models import (
|
|
|
43
44
|
SheetList,
|
|
44
45
|
data_types,
|
|
45
46
|
)
|
|
46
|
-
from cognite.neat._rules.models._rdfpath import Entity as RDFPathEntity
|
|
47
|
-
from cognite.neat._rules.models._rdfpath import RDFPath, SingleProperty
|
|
48
47
|
from cognite.neat._rules.models.data_types import AnyURI, DataType, Enum, File, String, Timeseries
|
|
49
48
|
from cognite.neat._rules.models.dms import DMSMetadata, DMSProperty, DMSValidation, DMSView
|
|
50
49
|
from cognite.neat._rules.models.dms._rules import DMSContainer, DMSEnum, DMSNode
|
|
@@ -60,7 +59,8 @@ from cognite.neat._rules.models.entities import (
|
|
|
60
59
|
ViewEntity,
|
|
61
60
|
)
|
|
62
61
|
from cognite.neat._rules.models.information import InformationClass, InformationMetadata, InformationProperty
|
|
63
|
-
from cognite.neat._utils.
|
|
62
|
+
from cognite.neat._utils.rdf_ import get_inheritance_path
|
|
63
|
+
from cognite.neat._utils.text import NamingStandardization, title, to_camel_case, to_words
|
|
64
64
|
|
|
65
65
|
from ._base import RulesTransformer, T_VerifiedIn, T_VerifiedOut, VerifiedRulesTransformer
|
|
66
66
|
from ._verification import VerifyDMSRules
|
|
@@ -75,22 +75,20 @@ class ConversionTransformer(VerifiedRulesTransformer[T_VerifiedIn, T_VerifiedOut
|
|
|
75
75
|
...
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
class
|
|
79
|
-
RulesTransformer[ReadRules[InformationInputRules], ReadRules[InformationInputRules]]
|
|
80
|
-
):
|
|
78
|
+
class ToDMSCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], ReadRules[InformationInputRules]]):
|
|
81
79
|
"""Converts input rules to rules that is compliant with the Information Model.
|
|
82
80
|
|
|
83
81
|
This is typically used with importers from arbitrary sources to ensure that classes and properties have valid
|
|
84
82
|
names.
|
|
85
83
|
|
|
86
84
|
Args:
|
|
87
|
-
|
|
88
|
-
- "
|
|
85
|
+
rename_warning: How to handle renaming of entities that are not compliant with the Information Model.
|
|
86
|
+
- "raise": Raises a warning and renames the entity.
|
|
89
87
|
- "skip": Renames the entity without raising a warning.
|
|
90
88
|
"""
|
|
91
89
|
|
|
92
|
-
def __init__(self,
|
|
93
|
-
self._renaming =
|
|
90
|
+
def __init__(self, rename_warning: Literal["raise", "skip"] = "skip") -> None:
|
|
91
|
+
self._renaming = rename_warning
|
|
94
92
|
|
|
95
93
|
@property
|
|
96
94
|
def description(self) -> str:
|
|
@@ -107,9 +105,9 @@ class ToInformationCompliantEntities(
|
|
|
107
105
|
new_by_old_class_suffix: dict[str, str] = {}
|
|
108
106
|
for cls in copy.classes:
|
|
109
107
|
cls_entity = cast(ClassEntity, cls.class_) # Safe due to the dump above
|
|
110
|
-
if not PATTERNS.
|
|
108
|
+
if not PATTERNS.view_id_compliance.match(cls_entity.suffix):
|
|
111
109
|
new_suffix = self._fix_cls_suffix(cls_entity.suffix)
|
|
112
|
-
if self._renaming == "
|
|
110
|
+
if self._renaming == "raise":
|
|
113
111
|
warnings.warn(
|
|
114
112
|
NeatValueWarning(f"Invalid class name {cls_entity.suffix!r}.Renaming to {new_suffix}"),
|
|
115
113
|
stacklevel=2,
|
|
@@ -123,7 +121,7 @@ class ToInformationCompliantEntities(
|
|
|
123
121
|
cls_.implements[i].suffix = new_by_old_class_suffix[parent.suffix] # type: ignore[union-attr]
|
|
124
122
|
|
|
125
123
|
for prop in copy.properties:
|
|
126
|
-
if not PATTERNS.
|
|
124
|
+
if not PATTERNS.dms_property_id_compliance.match(prop.property_):
|
|
127
125
|
new_property = self._fix_property(prop.property_)
|
|
128
126
|
if self._renaming == "warning":
|
|
129
127
|
warnings.warn(
|
|
@@ -174,6 +172,65 @@ class ToInformationCompliantEntities(
|
|
|
174
172
|
return property_
|
|
175
173
|
|
|
176
174
|
|
|
175
|
+
class StandardizeSpaceAndVersion(VerifiedRulesTransformer[DMSRules, DMSRules]): # type: ignore[misc]
|
|
176
|
+
"""This transformer standardizes the space and version of the DMSRules.
|
|
177
|
+
|
|
178
|
+
typically used to ensure all the views are moved to the same version as the data model.
|
|
179
|
+
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def description(self) -> str:
|
|
184
|
+
return "Ensures uniform version and space of the views belonging to the data model."
|
|
185
|
+
|
|
186
|
+
def transform(self, rules: DMSRules) -> DMSRules:
|
|
187
|
+
copy = rules.model_copy(deep=True)
|
|
188
|
+
|
|
189
|
+
space = copy.metadata.space
|
|
190
|
+
version = copy.metadata.version
|
|
191
|
+
|
|
192
|
+
copy.views = self._standardize_views(copy.views, space, version)
|
|
193
|
+
copy.properties = self._standardize_properties(copy.properties, space, version)
|
|
194
|
+
return copy
|
|
195
|
+
|
|
196
|
+
def _standardize_views(self, views: SheetList[DMSView], space: str, version: str) -> SheetList[DMSView]:
|
|
197
|
+
for view in views:
|
|
198
|
+
if view.view.space not in COGNITE_SPACES:
|
|
199
|
+
view.view.version = version
|
|
200
|
+
view.view.prefix = space
|
|
201
|
+
|
|
202
|
+
if view.implements:
|
|
203
|
+
for i, parent in enumerate(view.implements):
|
|
204
|
+
if parent.space not in COGNITE_SPACES:
|
|
205
|
+
view.implements[i].version = version
|
|
206
|
+
view.implements[i].prefix = space
|
|
207
|
+
return views
|
|
208
|
+
|
|
209
|
+
def _standardize_properties(
|
|
210
|
+
self, properties: SheetList[DMSProperty], space: str, version: str
|
|
211
|
+
) -> SheetList[DMSProperty]:
|
|
212
|
+
for property_ in properties:
|
|
213
|
+
if property_.view.space not in COGNITE_SPACES:
|
|
214
|
+
property_.view.version = version
|
|
215
|
+
property_.view.prefix = space
|
|
216
|
+
|
|
217
|
+
if isinstance(property_.value_type, ViewEntity) and property_.value_type.space not in COGNITE_SPACES:
|
|
218
|
+
property_.value_type.version = version
|
|
219
|
+
property_.value_type.prefix = space
|
|
220
|
+
|
|
221
|
+
# for edge connection
|
|
222
|
+
if (
|
|
223
|
+
property_.connection
|
|
224
|
+
and isinstance(property_.connection, EdgeEntity)
|
|
225
|
+
and property_.connection.properties
|
|
226
|
+
):
|
|
227
|
+
if property_.connection.properties.space not in COGNITE_SPACES:
|
|
228
|
+
property_.connection.properties.version = version
|
|
229
|
+
property_.connection.properties.prefix = space
|
|
230
|
+
|
|
231
|
+
return properties
|
|
232
|
+
|
|
233
|
+
|
|
177
234
|
class ToCompliantEntities(VerifiedRulesTransformer[InformationRules, InformationRules]): # type: ignore[misc]
|
|
178
235
|
"""Converts input rules to rules with compliant entity IDs that match regex patters used
|
|
179
236
|
by DMS schema components."""
|
|
@@ -504,8 +561,9 @@ _T_Entity = TypeVar("_T_Entity", bound=ClassEntity | ViewEntity)
|
|
|
504
561
|
|
|
505
562
|
|
|
506
563
|
class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
507
|
-
def __init__(self, new_id: DataModelId | tuple[str, str, str]):
|
|
564
|
+
def __init__(self, new_id: DataModelId | tuple[str, str, str], name: str | None = None):
|
|
508
565
|
self.new_id = DataModelId.load(new_id)
|
|
566
|
+
self.name = name
|
|
509
567
|
|
|
510
568
|
@property
|
|
511
569
|
def description(self) -> str:
|
|
@@ -518,10 +576,14 @@ class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
|
518
576
|
dump["metadata"]["space"] = self.new_id.space
|
|
519
577
|
dump["metadata"]["external_id"] = self.new_id.external_id
|
|
520
578
|
dump["metadata"]["version"] = self.new_id.version
|
|
579
|
+
dump["metadata"]["name"] = self.name or self._generate_name()
|
|
521
580
|
# Serialize and deserialize to set the new space and external_id
|
|
522
581
|
# as the default values for the new model.
|
|
523
582
|
return DMSRules.model_validate(DMSInputRules.load(dump).dump())
|
|
524
583
|
|
|
584
|
+
def _generate_name(self) -> str:
|
|
585
|
+
return title(to_words(self.new_id.external_id))
|
|
586
|
+
|
|
525
587
|
|
|
526
588
|
class ToExtensionModel(VerifiedRulesTransformer[DMSRules, DMSRules], ABC):
|
|
527
589
|
type_: ClassVar[str]
|
|
@@ -649,7 +711,7 @@ class ToEnterpriseModel(ToExtensionModel):
|
|
|
649
711
|
|
|
650
712
|
container = DMSContainer(container=container_entity)
|
|
651
713
|
|
|
652
|
-
property_id = f"{
|
|
714
|
+
property_id = f"{to_camel_case(view_entity.suffix)}{self.dummy_property}"
|
|
653
715
|
property_ = DMSProperty(
|
|
654
716
|
view=view_entity,
|
|
655
717
|
view_property=property_id,
|
|
@@ -759,13 +821,16 @@ class ToSolutionModel(ToExtensionModel):
|
|
|
759
821
|
|
|
760
822
|
@staticmethod
|
|
761
823
|
def _expand_properties(rules: DMSRules) -> DMSRules:
|
|
762
|
-
probe =
|
|
763
|
-
ancestor_properties_by_view = probe.
|
|
764
|
-
|
|
824
|
+
probe = RulesAnalysis(dms=rules)
|
|
825
|
+
ancestor_properties_by_view = probe.properties_by_view(
|
|
826
|
+
include_ancestors=True,
|
|
827
|
+
include_different_space=True,
|
|
765
828
|
)
|
|
766
829
|
property_ids_by_view = {
|
|
767
830
|
view: {prop.view_property for prop in properties}
|
|
768
|
-
for view, properties in probe.
|
|
831
|
+
for view, properties in probe.properties_by_view(
|
|
832
|
+
include_ancestors=False, include_different_space=True
|
|
833
|
+
).items()
|
|
769
834
|
}
|
|
770
835
|
for view, property_ids in property_ids_by_view.items():
|
|
771
836
|
ancestor_properties = ancestor_properties_by_view.get(view, [])
|
|
@@ -852,7 +917,7 @@ class ToSolutionModel(ToExtensionModel):
|
|
|
852
917
|
if view.view in read_view_by_new_view:
|
|
853
918
|
read_view = read_view_by_new_view[view.view]
|
|
854
919
|
container_entity = ContainerEntity(space=self.new_model_id.space, externalId=view.view.external_id)
|
|
855
|
-
prefix =
|
|
920
|
+
prefix = to_camel_case(view.view.suffix)
|
|
856
921
|
if self.properties == "repeat" and self.dummy_property:
|
|
857
922
|
property_ = DMSProperty(
|
|
858
923
|
view=view.view,
|
|
@@ -937,7 +1002,7 @@ class ToDataProductModel(ToSolutionModel):
|
|
|
937
1002
|
self.include = include
|
|
938
1003
|
|
|
939
1004
|
def transform(self, rules: DMSRules) -> DMSRules:
|
|
940
|
-
# Overwrite
|
|
1005
|
+
# Overwrite transform to avoid the warning.
|
|
941
1006
|
return self._to_solution(rules)
|
|
942
1007
|
|
|
943
1008
|
|
|
@@ -1011,7 +1076,7 @@ class DropModelViews(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
|
1011
1076
|
}
|
|
1012
1077
|
new_model = rules.model_copy(deep=True)
|
|
1013
1078
|
|
|
1014
|
-
properties_by_view =
|
|
1079
|
+
properties_by_view = RulesAnalysis(dms=new_model).properties_by_view(include_ancestors=True)
|
|
1015
1080
|
|
|
1016
1081
|
new_model.views = SheetList[DMSView]([view for view in new_model.views if view.view not in exclude_views])
|
|
1017
1082
|
new_properties = SheetList[DMSProperty]()
|
|
@@ -1155,6 +1220,7 @@ class ClassicPrepareCore(VerifiedRulesTransformer[InformationRules, InformationR
|
|
|
1155
1220
|
class_=ClassEntity(prefix=prefix, suffix="ClassicSourceSystem"),
|
|
1156
1221
|
description="A source system that provides data to the data model.",
|
|
1157
1222
|
neatId=namespace["ClassicSourceSystem"],
|
|
1223
|
+
instance_source=self.instance_namespace["ClassicSourceSystem"],
|
|
1158
1224
|
)
|
|
1159
1225
|
output.classes.append(source_system_class)
|
|
1160
1226
|
for prop in output.properties:
|
|
@@ -1183,15 +1249,7 @@ class ClassicPrepareCore(VerifiedRulesTransformer[InformationRules, InformationR
|
|
|
1183
1249
|
value_type=String(),
|
|
1184
1250
|
class_=ClassEntity(prefix=prefix, suffix="ClassicSourceSystem"),
|
|
1185
1251
|
max_count=1,
|
|
1186
|
-
instance_source=
|
|
1187
|
-
traversal=SingleProperty(
|
|
1188
|
-
class_=RDFPathEntity(
|
|
1189
|
-
prefix=instance_prefix,
|
|
1190
|
-
suffix="ClassicSourceSystem",
|
|
1191
|
-
),
|
|
1192
|
-
property=RDFPathEntity(prefix=instance_prefix, suffix="name"),
|
|
1193
|
-
),
|
|
1194
|
-
),
|
|
1252
|
+
instance_source=[self.instance_namespace["name"]],
|
|
1195
1253
|
)
|
|
1196
1254
|
)
|
|
1197
1255
|
return output
|
|
@@ -1673,7 +1731,6 @@ class _DMSRulesConverter:
|
|
|
1673
1731
|
classes.append(info_class)
|
|
1674
1732
|
|
|
1675
1733
|
prefixes = get_default_prefixes_and_namespaces()
|
|
1676
|
-
instance_prefix: str | None = None
|
|
1677
1734
|
if self.instance_namespace:
|
|
1678
1735
|
instance_prefix = next((k for k, v in prefixes.items() if v == self.instance_namespace), None)
|
|
1679
1736
|
if instance_prefix is None:
|
|
@@ -1696,15 +1753,6 @@ class _DMSRulesConverter:
|
|
|
1696
1753
|
else:
|
|
1697
1754
|
raise ValueError(f"Unsupported value type: {property_.value_type.type_}")
|
|
1698
1755
|
|
|
1699
|
-
transformation: RDFPath | None = None
|
|
1700
|
-
if instance_prefix is not None:
|
|
1701
|
-
transformation = RDFPath(
|
|
1702
|
-
traversal=SingleProperty(
|
|
1703
|
-
class_=RDFPathEntity(prefix=instance_prefix, suffix=property_.view.external_id),
|
|
1704
|
-
property=RDFPathEntity(prefix=instance_prefix, suffix=property_.view_property),
|
|
1705
|
-
)
|
|
1706
|
-
)
|
|
1707
|
-
|
|
1708
1756
|
info_property = InformationProperty(
|
|
1709
1757
|
# Removing version
|
|
1710
1758
|
class_=ClassEntity(suffix=property_.view.suffix, prefix=property_.view.prefix),
|
|
@@ -1713,7 +1761,6 @@ class _DMSRulesConverter:
|
|
|
1713
1761
|
description=property_.description,
|
|
1714
1762
|
min_count=(0 if property_.nullable or property_.nullable is None else 1),
|
|
1715
1763
|
max_count=(float("inf") if property_.is_list or property_.nullable is None else 1),
|
|
1716
|
-
instance_source=transformation,
|
|
1717
1764
|
)
|
|
1718
1765
|
|
|
1719
1766
|
# Linking
|
|
@@ -1746,3 +1793,146 @@ class _DMSRulesConverter:
|
|
|
1746
1793
|
created=metadata.created,
|
|
1747
1794
|
updated=metadata.updated,
|
|
1748
1795
|
)
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
class SubsetDMSRules(VerifiedRulesTransformer[DMSRules, DMSRules]):
|
|
1799
|
+
"""Subsets DMSRules to only include the specified views."""
|
|
1800
|
+
|
|
1801
|
+
def __init__(self, views: set[ViewEntity]):
|
|
1802
|
+
self._views = views
|
|
1803
|
+
|
|
1804
|
+
def transform(self, rules: DMSRules) -> DMSRules:
|
|
1805
|
+
analysis = RulesAnalysis(dms=rules)
|
|
1806
|
+
|
|
1807
|
+
views_by_view = analysis.view_by_view_entity
|
|
1808
|
+
implements_by_view = analysis.implements_by_view()
|
|
1809
|
+
|
|
1810
|
+
available = analysis.defined_views(include_ancestors=True)
|
|
1811
|
+
subset = available.intersection(self._views)
|
|
1812
|
+
|
|
1813
|
+
ancestors: set[ViewEntity] = set()
|
|
1814
|
+
for view in subset:
|
|
1815
|
+
ancestors = ancestors.union({ancestor for ancestor in get_inheritance_path(view, implements_by_view)})
|
|
1816
|
+
subset = subset.union(ancestors)
|
|
1817
|
+
|
|
1818
|
+
if not subset:
|
|
1819
|
+
raise NeatValueError("None of the requested views are defined in the rules!")
|
|
1820
|
+
|
|
1821
|
+
if nonexisting := self._views - subset:
|
|
1822
|
+
raise NeatValueError(
|
|
1823
|
+
"Following requested views do not exist"
|
|
1824
|
+
f" in the rules: [{','.join([view.external_id for view in nonexisting])}]. Aborting."
|
|
1825
|
+
)
|
|
1826
|
+
|
|
1827
|
+
subsetted_rules: dict[str, Any] = {
|
|
1828
|
+
"metadata": rules.metadata.model_copy(),
|
|
1829
|
+
"views": SheetList[DMSView](),
|
|
1830
|
+
"properties": SheetList[DMSProperty](),
|
|
1831
|
+
"containers": SheetList[DMSContainer](),
|
|
1832
|
+
"enum": rules.enum,
|
|
1833
|
+
"nodes": rules.nodes,
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
# add views
|
|
1837
|
+
for view in subset:
|
|
1838
|
+
subsetted_rules["views"].append(views_by_view[view])
|
|
1839
|
+
|
|
1840
|
+
used_containers = set()
|
|
1841
|
+
|
|
1842
|
+
# add properties
|
|
1843
|
+
for view, properties in analysis.properties_by_view(include_ancestors=False).items():
|
|
1844
|
+
if view not in subset:
|
|
1845
|
+
continue
|
|
1846
|
+
|
|
1847
|
+
for property_ in properties:
|
|
1848
|
+
if (
|
|
1849
|
+
isinstance(property_.value_type, DataType)
|
|
1850
|
+
or isinstance(property_.value_type, DMSUnknownEntity)
|
|
1851
|
+
or (isinstance(property_.value_type, ViewEntity) and property_.value_type in subset)
|
|
1852
|
+
):
|
|
1853
|
+
subsetted_rules["properties"].append(property_)
|
|
1854
|
+
|
|
1855
|
+
if property_.container:
|
|
1856
|
+
used_containers.add(property_.container)
|
|
1857
|
+
|
|
1858
|
+
# add containers
|
|
1859
|
+
if rules.containers:
|
|
1860
|
+
for container in rules.containers:
|
|
1861
|
+
if container.container in used_containers:
|
|
1862
|
+
subsetted_rules["containers"].append(container)
|
|
1863
|
+
|
|
1864
|
+
try:
|
|
1865
|
+
return DMSRules.model_validate(subsetted_rules)
|
|
1866
|
+
except ValidationError as e:
|
|
1867
|
+
raise NeatValueError(f"Cannot subset rules: {e}") from e
|
|
1868
|
+
|
|
1869
|
+
|
|
1870
|
+
class SubsetInformationRules(VerifiedRulesTransformer[InformationRules, InformationRules]):
|
|
1871
|
+
"""Subsets InformationRules to only include the specified classes."""
|
|
1872
|
+
|
|
1873
|
+
def __init__(self, classes: set[ClassEntity]):
|
|
1874
|
+
self._classes = classes
|
|
1875
|
+
|
|
1876
|
+
def transform(self, rules: InformationRules) -> InformationRules:
|
|
1877
|
+
analysis = RulesAnalysis(information=rules)
|
|
1878
|
+
|
|
1879
|
+
class_by_class_entity = analysis.class_by_class_entity
|
|
1880
|
+
parent_entity_by_class_entity = analysis.parents_by_class()
|
|
1881
|
+
|
|
1882
|
+
available = analysis.defined_classes(include_ancestors=True)
|
|
1883
|
+
subset = available.intersection(self._classes)
|
|
1884
|
+
|
|
1885
|
+
# need to add all the parent classes of the desired classes to the possible classes
|
|
1886
|
+
ancestors: set[ClassEntity] = set()
|
|
1887
|
+
for class_ in subset:
|
|
1888
|
+
ancestors = ancestors.union(
|
|
1889
|
+
{ancestor for ancestor in get_inheritance_path(class_, parent_entity_by_class_entity)}
|
|
1890
|
+
)
|
|
1891
|
+
subset = subset.union(ancestors)
|
|
1892
|
+
|
|
1893
|
+
if not subset:
|
|
1894
|
+
raise NeatValueError("None of the requested classes are defined in the rules!")
|
|
1895
|
+
|
|
1896
|
+
if nonexisting := self._classes - subset:
|
|
1897
|
+
raise NeatValueError(
|
|
1898
|
+
"Following requested classes do not exist"
|
|
1899
|
+
f" in the rules: [{','.join([class_.suffix for class_ in nonexisting])}]"
|
|
1900
|
+
". Aborting."
|
|
1901
|
+
)
|
|
1902
|
+
|
|
1903
|
+
subsetted_rules: dict[str, Any] = {
|
|
1904
|
+
"metadata": rules.metadata.model_copy(),
|
|
1905
|
+
"prefixes": (rules.prefixes or {}).copy(),
|
|
1906
|
+
"classes": SheetList[InformationClass](),
|
|
1907
|
+
"properties": SheetList[InformationProperty](),
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
for class_ in subset:
|
|
1911
|
+
subsetted_rules["classes"].append(class_by_class_entity[class_])
|
|
1912
|
+
|
|
1913
|
+
for class_, properties in analysis.properties_by_class(include_ancestors=False).items():
|
|
1914
|
+
if class_ not in subset:
|
|
1915
|
+
continue
|
|
1916
|
+
for property_ in properties:
|
|
1917
|
+
# datatype property can be added directly
|
|
1918
|
+
if (
|
|
1919
|
+
isinstance(property_.value_type, DataType)
|
|
1920
|
+
or (isinstance(property_.value_type, ClassEntity) and property_.value_type in subset)
|
|
1921
|
+
or isinstance(property_.value_type, UnknownEntity)
|
|
1922
|
+
):
|
|
1923
|
+
subsetted_rules["properties"].append(property_)
|
|
1924
|
+
# object property can be added if the value type is in the subset
|
|
1925
|
+
elif isinstance(property_.value_type, MultiValueTypeInfo):
|
|
1926
|
+
allowed = [t for t in property_.value_type.types if t in subset or isinstance(t, DataType)]
|
|
1927
|
+
if allowed:
|
|
1928
|
+
subsetted_rules["properties"].append(
|
|
1929
|
+
property_.model_copy(
|
|
1930
|
+
deep=True,
|
|
1931
|
+
update={"value_type": MultiValueTypeInfo(types=allowed)},
|
|
1932
|
+
)
|
|
1933
|
+
)
|
|
1934
|
+
|
|
1935
|
+
try:
|
|
1936
|
+
return InformationRules.model_validate(subsetted_rules)
|
|
1937
|
+
except ValidationError as e:
|
|
1938
|
+
raise NeatValueError(f"Cannot subset rules: {e}") from e
|
|
@@ -35,27 +35,22 @@ class VerificationTransformer(RulesTransformer[T_ReadInputRules, T_VerifiedRules
|
|
|
35
35
|
in_ = rules.rules
|
|
36
36
|
if in_ is None:
|
|
37
37
|
raise NeatValueError("Cannot verify rules. The reading of the rules failed.")
|
|
38
|
-
error_args = rules.read_context
|
|
39
38
|
verified_rules: T_VerifiedRules | None = None
|
|
40
39
|
# We need to catch issues as we use the error args to provide extra context for the errors/warnings
|
|
41
|
-
# For example, which row in the spreadsheet the error occurred
|
|
42
|
-
with catch_issues(
|
|
40
|
+
# For example, which row in the spreadsheet the error occurred.
|
|
41
|
+
with catch_issues(rules.read_context) as issues:
|
|
43
42
|
rules_cls = self._get_rules_cls(rules)
|
|
44
43
|
dumped = in_.dump()
|
|
45
44
|
verified_rules = rules_cls.model_validate(dumped) # type: ignore[assignment]
|
|
46
45
|
if self.validate:
|
|
47
46
|
validation_cls = self._get_validation_cls(verified_rules) # type: ignore[arg-type]
|
|
48
47
|
if issubclass(validation_cls, DMSValidation):
|
|
49
|
-
validation_issues = DMSValidation(verified_rules, self._client).validate() # type: ignore[arg-type]
|
|
48
|
+
validation_issues = DMSValidation(verified_rules, self._client, rules.read_context).validate() # type: ignore[arg-type]
|
|
50
49
|
elif issubclass(validation_cls, InformationValidation):
|
|
51
|
-
validation_issues = InformationValidation(verified_rules).validate() # type: ignore[arg-type]
|
|
50
|
+
validation_issues = InformationValidation(verified_rules, rules.read_context).validate() # type: ignore[arg-type]
|
|
52
51
|
else:
|
|
53
52
|
raise NeatValueError("Unsupported rule type")
|
|
54
|
-
|
|
55
|
-
# Need to trigger and raise such that the catch_issues can add the extra context
|
|
56
|
-
validation_issues.trigger_warnings()
|
|
57
|
-
if validation_issues.has_errors:
|
|
58
|
-
raise MultiValueError(validation_issues.errors)
|
|
53
|
+
issues.extend(validation_issues)
|
|
59
54
|
|
|
60
55
|
# Raise issues which is expected to be handled outside of this method
|
|
61
56
|
issues.trigger_warnings()
|
cognite/neat/_session/_base.py
CHANGED
|
@@ -16,7 +16,7 @@ from cognite.neat._rules.transformers import (
|
|
|
16
16
|
InformationToDMS,
|
|
17
17
|
MergeDMSRules,
|
|
18
18
|
MergeInformationRules,
|
|
19
|
-
|
|
19
|
+
ToDMSCompliantEntities,
|
|
20
20
|
VerifyInformationRules,
|
|
21
21
|
)
|
|
22
22
|
from cognite.neat._store._rules_store import RulesEntity
|
|
@@ -25,6 +25,7 @@ from cognite.neat._utils.auxiliary import local_import
|
|
|
25
25
|
from ._collector import _COLLECTOR, Collector
|
|
26
26
|
from ._create import CreateAPI
|
|
27
27
|
from ._drop import DropAPI
|
|
28
|
+
from ._explore import ExploreAPI
|
|
28
29
|
from ._fix import FixAPI
|
|
29
30
|
from ._inspect import InspectAPI
|
|
30
31
|
from ._mapping import MappingAPI
|
|
@@ -33,6 +34,7 @@ from ._read import ReadAPI
|
|
|
33
34
|
from ._set import SetAPI
|
|
34
35
|
from ._show import ShowAPI
|
|
35
36
|
from ._state import SessionState
|
|
37
|
+
from ._subset import SubsetAPI
|
|
36
38
|
from ._to import ToAPI
|
|
37
39
|
from .engine import load_neat_engine
|
|
38
40
|
from .exceptions import session_class_wrapper
|
|
@@ -101,7 +103,9 @@ class NeatSession:
|
|
|
101
103
|
self.inspect = InspectAPI(self._state)
|
|
102
104
|
self.mapping = MappingAPI(self._state)
|
|
103
105
|
self.drop = DropAPI(self._state)
|
|
106
|
+
self.subset = SubsetAPI(self._state)
|
|
104
107
|
self.create = CreateAPI(self._state)
|
|
108
|
+
self._explore = ExploreAPI(self._state)
|
|
105
109
|
self.opt = OptAPI()
|
|
106
110
|
self.opt._display()
|
|
107
111
|
if load_engine != "skip" and (engine_version := load_neat_engine(client, load_engine)):
|
|
@@ -236,9 +240,7 @@ class NeatSession:
|
|
|
236
240
|
|
|
237
241
|
def action() -> tuple[InformationRules, DMSRules | None]:
|
|
238
242
|
unverified_information = importer.to_rules()
|
|
239
|
-
unverified_information =
|
|
240
|
-
unverified_information
|
|
241
|
-
)
|
|
243
|
+
unverified_information = ToDMSCompliantEntities(rename_warning="raise").transform(unverified_information)
|
|
242
244
|
|
|
243
245
|
extra_info = VerifyInformationRules().transform(unverified_information)
|
|
244
246
|
if not last_entity:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from rdflib import URIRef
|
|
5
|
+
|
|
6
|
+
from cognite.neat._utils.rdf_ import remove_namespace_from_uri
|
|
7
|
+
from cognite.neat._utils.text import humanize_collection
|
|
8
|
+
|
|
9
|
+
from ._state import SessionState
|
|
10
|
+
from .exceptions import NeatSessionError, session_class_wrapper
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@session_class_wrapper
|
|
14
|
+
class ExploreAPI:
|
|
15
|
+
"""
|
|
16
|
+
Explore the instances in the session.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, state: SessionState):
|
|
20
|
+
self._state = state
|
|
21
|
+
|
|
22
|
+
def types(self) -> pd.DataFrame:
|
|
23
|
+
"""List all the types of instances in the session."""
|
|
24
|
+
return pd.DataFrame(self._state.instances.store.queries.types_with_instance_and_property_count())
|
|
25
|
+
|
|
26
|
+
def properties(self) -> pd.DataFrame:
|
|
27
|
+
"""List all the properties of a type of instances in the session."""
|
|
28
|
+
return pd.DataFrame(self._state.instances.store.queries.properties_with_count())
|
|
29
|
+
|
|
30
|
+
def instance_with_properties(self, type: str) -> dict[str, set[str]]:
|
|
31
|
+
"""List all the instances of a type with their properties."""
|
|
32
|
+
available_types = self._state.instances.store.queries.list_types(remove_namespace=False)
|
|
33
|
+
uri_by_type = {remove_namespace_from_uri(t[0]): t[0] for t in available_types}
|
|
34
|
+
if type not in uri_by_type:
|
|
35
|
+
raise NeatSessionError(
|
|
36
|
+
f"Type {type} not found. Available types are: {humanize_collection(uri_by_type.keys())}"
|
|
37
|
+
)
|
|
38
|
+
type_uri = cast(URIRef, uri_by_type[type])
|
|
39
|
+
return self._state.instances.store.queries.instances_with_properties(type_uri, remove_namespace=True)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import difflib
|
|
2
|
-
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Callable, Set
|
|
3
3
|
from typing import Literal, overload
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
@@ -85,11 +85,13 @@ class InspectIssues:
|
|
|
85
85
|
|
|
86
86
|
def __init__(self, state: SessionState) -> None:
|
|
87
87
|
self._state = state
|
|
88
|
+
self._max_display = 50
|
|
88
89
|
|
|
89
90
|
@overload
|
|
90
91
|
def __call__(
|
|
91
92
|
self,
|
|
92
93
|
search: str | None = None,
|
|
94
|
+
include: Literal["all", "errors", "warning"] | Set[Literal["all", "errors", "warning"]] = "all",
|
|
93
95
|
return_dataframe: Literal[True] = (False if IN_NOTEBOOK else True), # type: ignore[assignment]
|
|
94
96
|
) -> pd.DataFrame: ...
|
|
95
97
|
|
|
@@ -97,12 +99,14 @@ class InspectIssues:
|
|
|
97
99
|
def __call__(
|
|
98
100
|
self,
|
|
99
101
|
search: str | None = None,
|
|
102
|
+
include: Literal["all", "errors", "warning"] | Set[Literal["all", "errors", "warning"]] = "all",
|
|
100
103
|
return_dataframe: Literal[False] = (False if IN_NOTEBOOK else True), # type: ignore[assignment]
|
|
101
104
|
) -> None: ...
|
|
102
105
|
|
|
103
106
|
def __call__(
|
|
104
107
|
self,
|
|
105
108
|
search: str | None = None,
|
|
109
|
+
include: Literal["all", "errors", "warning"] | Set[Literal["all", "errors", "warning"]] = "all",
|
|
106
110
|
return_dataframe: bool = (False if IN_NOTEBOOK else True), # type: ignore[assignment]
|
|
107
111
|
) -> pd.DataFrame | None:
|
|
108
112
|
"""Returns the issues of the current data model."""
|
|
@@ -113,6 +117,13 @@ class InspectIssues:
|
|
|
113
117
|
elif issues is None:
|
|
114
118
|
self._print("No issues found.")
|
|
115
119
|
return pd.DataFrame() if return_dataframe else None
|
|
120
|
+
include_set = {include} if isinstance(include, str) else include
|
|
121
|
+
if "all" in include_set:
|
|
122
|
+
include_set = {"errors", "warning"}
|
|
123
|
+
if "warning" not in include_set:
|
|
124
|
+
issues = issues.errors
|
|
125
|
+
if "errors" not in include_set:
|
|
126
|
+
issues = issues.warnings
|
|
116
127
|
|
|
117
128
|
if issues and search is not None:
|
|
118
129
|
unique_types = {type(issue).__name__ for issue in issues}
|
|
@@ -120,18 +131,21 @@ class InspectIssues:
|
|
|
120
131
|
issues = IssueList([issue for issue in issues if type(issue).__name__ in closest_match])
|
|
121
132
|
|
|
122
133
|
issue_str = "\n".join(
|
|
123
|
-
[
|
|
134
|
+
[
|
|
135
|
+
f" * **{type(issue).__name__}**: {issue.as_message(include_type=False)}"
|
|
136
|
+
for issue in issues[: self._max_display]
|
|
137
|
+
]
|
|
138
|
+
+ ([] if len(issues) <= 50 else [f" * ... {len(issues) - self._max_display} more"])
|
|
124
139
|
)
|
|
125
140
|
markdown_str = f"### {len(issues)} issues found\n\n{issue_str}"
|
|
126
|
-
|
|
127
141
|
if IN_NOTEBOOK:
|
|
128
142
|
from IPython.display import Markdown, display
|
|
129
143
|
|
|
130
144
|
display(Markdown(markdown_str))
|
|
131
145
|
elif RICH_AVAILABLE:
|
|
132
|
-
from rich import print
|
|
146
|
+
from rich import print as rprint
|
|
133
147
|
|
|
134
|
-
|
|
148
|
+
rprint(RichMarkdown(markdown_str))
|
|
135
149
|
|
|
136
150
|
if return_dataframe:
|
|
137
151
|
return issues.to_pandas()
|
|
@@ -170,6 +184,7 @@ class InspectOutcome:
|
|
|
170
184
|
class InspectUploadOutcome:
|
|
171
185
|
def __init__(self, get_last_outcome: Callable[[], UploadResultList]) -> None:
|
|
172
186
|
self._get_last_outcome = get_last_outcome
|
|
187
|
+
self._max_display = 50
|
|
173
188
|
|
|
174
189
|
@staticmethod
|
|
175
190
|
def _as_set(value: str | list[str] | None) -> set[str] | None:
|
|
@@ -223,7 +238,7 @@ class InspectUploadOutcome:
|
|
|
223
238
|
from IPython.display import Markdown, display
|
|
224
239
|
|
|
225
240
|
lines: list[str] = []
|
|
226
|
-
for item in outcome:
|
|
241
|
+
for line_no, item in enumerate(outcome):
|
|
227
242
|
lines.append(f"### {item.name}")
|
|
228
243
|
if unique_errors := set(item.error_messages):
|
|
229
244
|
lines.append("#### Errors")
|
|
@@ -255,6 +270,10 @@ class InspectUploadOutcome:
|
|
|
255
270
|
else:
|
|
256
271
|
lines.append(f" * {value}")
|
|
257
272
|
|
|
273
|
+
if line_no >= self._max_display:
|
|
274
|
+
lines.append(f"### ... {len(outcome) - self._max_display} more")
|
|
275
|
+
break
|
|
276
|
+
|
|
258
277
|
display(Markdown("\n".join(lines)))
|
|
259
278
|
|
|
260
279
|
if return_dataframe:
|