cognite-neat 0.109.3__py3-none-any.whl → 0.110.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 +2 -0
- cognite/neat/_client/_api/schema.py +17 -1
- cognite/neat/_client/data_classes/schema.py +3 -3
- cognite/neat/_constants.py +11 -0
- cognite/neat/_graph/extractors/_classic_cdf/_classic.py +9 -10
- cognite/neat/_graph/extractors/_iodd.py +3 -3
- cognite/neat/_graph/extractors/_mock_graph_generator.py +9 -7
- cognite/neat/_graph/loaders/_rdf2dms.py +285 -346
- cognite/neat/_graph/queries/_base.py +28 -92
- cognite/neat/_graph/transformers/__init__.py +1 -3
- 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 +61 -0
- cognite/neat/_issues/errors/__init__.py +18 -4
- 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/_rules/_constants.py +9 -0
- cognite/neat/_rules/_shared.py +3 -2
- cognite/neat/_rules/analysis/__init__.py +2 -3
- cognite/neat/_rules/analysis/_base.py +450 -258
- cognite/neat/_rules/catalog/info-rules-imf.xlsx +0 -0
- cognite/neat/_rules/exporters/_rules2excel.py +2 -8
- 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 +59 -25
- cognite/neat/_rules/importers/_rdf/_shared.py +1 -1
- cognite/neat/_rules/importers/_spreadsheet2rules.py +12 -9
- cognite/neat/_rules/models/dms/_rules.py +3 -1
- cognite/neat/_rules/models/dms/_rules_input.py +4 -0
- cognite/neat/_rules/models/dms/_validation.py +14 -4
- cognite/neat/_rules/models/entities/_loaders.py +1 -1
- cognite/neat/_rules/models/entities/_multi_value.py +2 -2
- cognite/neat/_rules/models/information/_rules.py +18 -17
- cognite/neat/_rules/models/information/_rules_input.py +2 -1
- cognite/neat/_rules/models/information/_validation.py +3 -1
- cognite/neat/_rules/transformers/__init__.py +8 -2
- cognite/neat/_rules/transformers/_converters.py +242 -43
- cognite/neat/_rules/transformers/_verification.py +5 -10
- cognite/neat/_session/_base.py +4 -4
- cognite/neat/_session/_prepare.py +12 -0
- cognite/neat/_session/_read.py +21 -17
- cognite/neat/_session/_show.py +11 -123
- cognite/neat/_session/_state.py +0 -2
- cognite/neat/_session/_subset.py +64 -0
- cognite/neat/_session/_to.py +63 -12
- cognite/neat/_store/_graph_store.py +5 -246
- cognite/neat/_utils/rdf_.py +2 -2
- cognite/neat/_utils/spreadsheet.py +44 -1
- cognite/neat/_utils/text.py +51 -32
- cognite/neat/_version.py +1 -1
- {cognite_neat-0.109.3.dist-info → cognite_neat-0.110.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.109.3.dist-info → cognite_neat-0.110.0.dist-info}/RECORD +62 -64
- {cognite_neat-0.109.3.dist-info → cognite_neat-0.110.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.3.dist-info → cognite_neat-0.110.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.109.3.dist-info → cognite_neat-0.110.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,51 +1,49 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
import warnings
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
3
|
from collections import defaultdict
|
|
5
|
-
from collections.abc import Set
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from
|
|
4
|
+
from collections.abc import Hashable, ItemsView, Iterator, KeysView, MutableMapping, Set, ValuesView
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from graphlib import TopologicalSorter
|
|
7
|
+
from typing import Any, Literal, TypeVar, overload
|
|
8
8
|
|
|
9
|
+
import networkx as nx
|
|
9
10
|
import pandas as pd
|
|
11
|
+
from cognite.client import data_modeling as dm
|
|
10
12
|
from rdflib import URIRef
|
|
11
13
|
|
|
12
|
-
from cognite.neat.
|
|
13
|
-
from cognite.neat.
|
|
14
|
-
from cognite.neat._rules.models
|
|
15
|
-
from cognite.neat._rules.models.
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
from cognite.neat._issues.errors import NeatValueError
|
|
15
|
+
from cognite.neat._issues.warnings import NeatValueWarning
|
|
16
|
+
from cognite.neat._rules.models import DMSRules, InformationRules
|
|
17
|
+
from cognite.neat._rules.models.dms import DMSProperty
|
|
18
|
+
from cognite.neat._rules.models.dms._rules import DMSView
|
|
19
|
+
from cognite.neat._rules.models.entities import ClassEntity, MultiValueTypeInfo, ViewEntity
|
|
20
|
+
from cognite.neat._rules.models.entities._single_value import (
|
|
21
|
+
UnknownEntity,
|
|
18
22
|
)
|
|
19
|
-
from cognite.neat._rules.models.information import InformationProperty
|
|
20
|
-
from cognite.neat._rules.models.information._rules import InformationClass
|
|
21
|
-
from cognite.neat._utils.rdf_ import get_inheritance_path
|
|
23
|
+
from cognite.neat._rules.models.information import InformationClass, InformationProperty
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
T_Property = TypeVar("T_Property", bound=InformationProperty | DMSProperty)
|
|
25
|
-
T_Class = TypeVar("T_Class", bound=InformationClass | DMSView)
|
|
26
|
-
T_ClassEntity = TypeVar("T_ClassEntity", bound=Entity)
|
|
27
|
-
T_PropertyEntity = TypeVar("T_PropertyEntity", bound=Entity | str)
|
|
25
|
+
T_Hashable = TypeVar("T_Hashable", bound=Hashable)
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
@dataclass(frozen=True)
|
|
31
|
-
class Linkage
|
|
32
|
-
source_class:
|
|
33
|
-
connecting_property:
|
|
34
|
-
target_class:
|
|
29
|
+
class Linkage:
|
|
30
|
+
source_class: ClassEntity
|
|
31
|
+
connecting_property: str
|
|
32
|
+
target_class: ClassEntity
|
|
35
33
|
max_occurrence: int | float | None
|
|
36
34
|
|
|
37
35
|
|
|
38
|
-
class LinkageSet(set,
|
|
36
|
+
class LinkageSet(set, Set[Linkage]):
|
|
39
37
|
@property
|
|
40
|
-
def source_class(self) -> set[
|
|
38
|
+
def source_class(self) -> set[ClassEntity]:
|
|
41
39
|
return {link.source_class for link in self}
|
|
42
40
|
|
|
43
41
|
@property
|
|
44
|
-
def target_class(self) -> set[
|
|
42
|
+
def target_class(self) -> set[ClassEntity]:
|
|
45
43
|
return {link.target_class for link in self}
|
|
46
44
|
|
|
47
|
-
def get_target_classes_by_source(self) -> dict[
|
|
48
|
-
target_classes_by_source: dict[
|
|
45
|
+
def get_target_classes_by_source(self) -> dict[ClassEntity, set[ClassEntity]]:
|
|
46
|
+
target_classes_by_source: dict[ClassEntity, set[ClassEntity]] = defaultdict(set)
|
|
49
47
|
for link in self:
|
|
50
48
|
target_classes_by_source[link.source_class].add(link.target_class)
|
|
51
49
|
return target_classes_by_source
|
|
@@ -65,271 +63,327 @@ class LinkageSet(set, Generic[T_ClassEntity, T_PropertyEntity], Set[Linkage[T_Cl
|
|
|
65
63
|
)
|
|
66
64
|
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
@dataclass
|
|
67
|
+
class ViewQuery:
|
|
68
|
+
view_id: dm.ViewId
|
|
69
|
+
rdf_type: URIRef
|
|
70
|
+
property_renaming_config: dict[URIRef, str] = field(default_factory=dict)
|
|
71
71
|
|
|
72
|
-
@abstractmethod
|
|
73
|
-
def _get_classes(self) -> list[T_Class]:
|
|
74
|
-
raise NotImplementedError
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
class ViewQueryDict(dict, MutableMapping[dm.ViewId, ViewQuery]):
|
|
74
|
+
# The below methods are included to make better type hints in the IDE
|
|
75
|
+
def __getitem__(self, k: dm.ViewId) -> ViewQuery:
|
|
76
|
+
return super().__getitem__(k)
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
raise NotImplementedError
|
|
78
|
+
def __setitem__(self, k: dm.ViewId, v: ViewQuery) -> None:
|
|
79
|
+
super().__setitem__(k, v)
|
|
83
80
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
raise NotImplementedError
|
|
81
|
+
def __delitem__(self, k: dm.ViewId) -> None:
|
|
82
|
+
super().__delitem__(k)
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
raise NotImplementedError
|
|
84
|
+
def __iter__(self) -> Iterator[dm.ViewId]:
|
|
85
|
+
return super().__iter__()
|
|
91
86
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _set_cls_entity(cls, property_: T_Property, class_: T_ClassEntity) -> None:
|
|
95
|
-
raise NotImplementedError
|
|
87
|
+
def keys(self) -> KeysView[dm.ViewId]: # type: ignore[override]
|
|
88
|
+
return super().keys()
|
|
96
89
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
raise NotImplementedError
|
|
90
|
+
def values(self) -> ValuesView[ViewQuery]: # type: ignore[override]
|
|
91
|
+
return super().values()
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
raise NotImplementedError
|
|
93
|
+
def items(self) -> ItemsView[dm.ViewId, ViewQuery]: # type: ignore[override]
|
|
94
|
+
return super().items()
|
|
104
95
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
96
|
+
def get(self, __key: dm.ViewId, __default: Any = ...) -> ViewQuery:
|
|
97
|
+
return super().get(__key, __default)
|
|
98
|
+
|
|
99
|
+
def pop(self, __key: dm.ViewId, __default: Any = ...) -> ViewQuery:
|
|
100
|
+
return super().pop(__key, __default)
|
|
101
|
+
|
|
102
|
+
def popitem(self) -> tuple[dm.ViewId, ViewQuery]:
|
|
103
|
+
return super().popitem()
|
|
108
104
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
|
|
106
|
+
class RulesAnalysis:
|
|
107
|
+
def __init__(self, information: InformationRules | None = None, dms: DMSRules | None = None) -> None:
|
|
108
|
+
self._information = information
|
|
109
|
+
self._dms = dms
|
|
112
110
|
|
|
113
111
|
@property
|
|
114
|
-
def
|
|
115
|
-
|
|
112
|
+
def information(self) -> InformationRules:
|
|
113
|
+
if self._information is None:
|
|
114
|
+
raise NeatValueError("Information rules are required for this analysis")
|
|
115
|
+
return self._information
|
|
116
116
|
|
|
117
117
|
@property
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
def dms(self) -> DMSRules:
|
|
119
|
+
if self._dms is None:
|
|
120
|
+
raise NeatValueError("DMS rules are required for this analysis")
|
|
121
|
+
return self._dms
|
|
122
|
+
|
|
123
|
+
def parents_by_class(
|
|
124
|
+
self, include_ancestors: bool = False, include_different_space: bool = False
|
|
125
|
+
) -> dict[ClassEntity, set[ClassEntity]]:
|
|
126
|
+
"""Get a dictionary of classes and their parents.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
include_ancestors (bool, optional): Include ancestors of the parents. Defaults to False.
|
|
130
|
+
include_different_space (bool, optional): Include parents from different spaces. Defaults to False.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
dict[ClassEntity, set[ClassEntity]]: Values parents with class as key.
|
|
134
|
+
"""
|
|
135
|
+
parents_by_class: dict[ClassEntity, set[ClassEntity]] = {}
|
|
136
|
+
for class_ in self.information.classes:
|
|
137
|
+
parents_by_class[class_.class_] = set()
|
|
138
|
+
for parent in class_.implements or []:
|
|
139
|
+
if include_different_space or parent.prefix == class_.class_.prefix:
|
|
140
|
+
parents_by_class[class_.class_].add(parent)
|
|
131
141
|
else:
|
|
132
142
|
warnings.warn(
|
|
133
|
-
|
|
143
|
+
NeatValueWarning(
|
|
144
|
+
f"Parent class {parent} of class {class_} is not in the same namespace, skipping!"
|
|
145
|
+
),
|
|
134
146
|
stacklevel=2,
|
|
135
147
|
)
|
|
148
|
+
if include_ancestors:
|
|
149
|
+
self._include_ancestors(parents_by_class)
|
|
150
|
+
|
|
151
|
+
return parents_by_class
|
|
136
152
|
|
|
137
|
-
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _include_ancestors(parents_by_class: dict[T_Hashable, set[T_Hashable]]) -> None:
|
|
155
|
+
# Topological sort to ensure that classes include all ancestors
|
|
156
|
+
for class_entity in list(TopologicalSorter(parents_by_class).static_order()):
|
|
157
|
+
parents_by_class[class_entity] |= {
|
|
158
|
+
grand_parent for parent in parents_by_class[class_entity] for grand_parent in parents_by_class[parent]
|
|
159
|
+
}
|
|
138
160
|
|
|
139
|
-
def
|
|
140
|
-
self,
|
|
141
|
-
) -> dict[
|
|
142
|
-
"""
|
|
161
|
+
def properties_by_class(
|
|
162
|
+
self, include_ancestors: bool = False, include_different_space: bool = False
|
|
163
|
+
) -> dict[ClassEntity, list[InformationProperty]]:
|
|
164
|
+
"""Get a dictionary of classes and their properties.
|
|
143
165
|
|
|
144
166
|
Args:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
different namespaces or not. Defaults False
|
|
167
|
+
include_ancestors: Whether to include properties from parent classes.
|
|
168
|
+
include_different_space: Whether to include properties from parent classes in different spaces.
|
|
148
169
|
|
|
149
170
|
Returns:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
!!! note "consider_inheritance"
|
|
153
|
-
If consider_inheritance is True, properties from parent classes will also be considered.
|
|
154
|
-
This means if a class has a parent class, and the parent class has properties defined for it,
|
|
155
|
-
while we do not have any properties defined for the child class, we will still consider the
|
|
156
|
-
properties from the parent class. If consider_inheritance is False, we will only consider
|
|
157
|
-
properties defined for the child class, thus if no properties are defined for the child class,
|
|
158
|
-
it will not be included in the returned dictionary.
|
|
159
|
-
"""
|
|
171
|
+
dict[ClassEntity, list[InformationProperty]]: Values properties with class as key.
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
for
|
|
164
|
-
|
|
173
|
+
"""
|
|
174
|
+
properties_by_classes: dict[ClassEntity, list[InformationProperty]] = defaultdict(list)
|
|
175
|
+
for prop in self.information.properties:
|
|
176
|
+
properties_by_classes[prop.class_].append(prop)
|
|
177
|
+
|
|
178
|
+
if include_ancestors:
|
|
179
|
+
parents_by_classes = self.parents_by_class(
|
|
180
|
+
include_ancestors=include_ancestors, include_different_space=include_different_space
|
|
181
|
+
)
|
|
182
|
+
for class_, parents in parents_by_classes.items():
|
|
183
|
+
class_properties = {prop.property_ for prop in properties_by_classes[class_]}
|
|
184
|
+
for parent in parents:
|
|
185
|
+
for parent_prop in properties_by_classes[parent]:
|
|
186
|
+
if parent_prop.property_ not in class_properties:
|
|
187
|
+
child_prop = parent_prop.model_copy(update={"class_": class_})
|
|
188
|
+
properties_by_classes[class_].append(child_prop)
|
|
189
|
+
class_properties.add(child_prop.property_)
|
|
190
|
+
|
|
191
|
+
return properties_by_classes
|
|
192
|
+
|
|
193
|
+
def implements_by_view(
|
|
194
|
+
self, include_ancestors: bool = False, include_different_space: bool = False
|
|
195
|
+
) -> dict[ViewEntity, set[ViewEntity]]:
|
|
196
|
+
"""Get a dictionary of views and their implemented views."""
|
|
197
|
+
# This is a duplicate fo the parent_by_class method, but for views
|
|
198
|
+
# The choice to duplicate the code is to avoid generics which will make the code less readable
|
|
199
|
+
implements_by_view: dict[ViewEntity, set[ViewEntity]] = {}
|
|
200
|
+
for view in self.dms.views:
|
|
201
|
+
implements_by_view[view.view] = set()
|
|
202
|
+
for implements in view.implements or []:
|
|
203
|
+
if include_different_space or implements.space == view.view.space:
|
|
204
|
+
implements_by_view[view.view].add(implements)
|
|
205
|
+
else:
|
|
206
|
+
warnings.warn(
|
|
207
|
+
NeatValueWarning(
|
|
208
|
+
f"Implemented view {implements} of view {view} is not in the same namespace, skipping!"
|
|
209
|
+
),
|
|
210
|
+
stacklevel=2,
|
|
211
|
+
)
|
|
212
|
+
if include_ancestors:
|
|
213
|
+
self._include_ancestors(implements_by_view)
|
|
214
|
+
return implements_by_view
|
|
215
|
+
|
|
216
|
+
def properties_by_view(
|
|
217
|
+
self, include_ancestors: bool = False, include_different_space: bool = False
|
|
218
|
+
) -> dict[ViewEntity, list[DMSProperty]]:
|
|
219
|
+
"""Get a dictionary of views and their properties."""
|
|
220
|
+
# This is a duplicate fo the properties_by_class method, but for views
|
|
221
|
+
# The choice to duplicate the code is to avoid generics which will make the code less readable.
|
|
222
|
+
properties_by_views: dict[ViewEntity, list[DMSProperty]] = defaultdict(list)
|
|
223
|
+
for prop in self.dms.properties:
|
|
224
|
+
properties_by_views[prop.view].append(prop)
|
|
225
|
+
|
|
226
|
+
if include_ancestors:
|
|
227
|
+
implements_by_view = self.implements_by_view(
|
|
228
|
+
include_ancestors=include_ancestors, include_different_space=include_different_space
|
|
229
|
+
)
|
|
230
|
+
for view, parents in implements_by_view.items():
|
|
231
|
+
view_properties = {prop.view_property for prop in properties_by_views[view]}
|
|
232
|
+
for parent in parents:
|
|
233
|
+
for parent_prop in properties_by_views[parent]:
|
|
234
|
+
if parent_prop.view_property not in view_properties:
|
|
235
|
+
child_prop = parent_prop.model_copy(update={"view": view})
|
|
236
|
+
properties_by_views[view].append(child_prop)
|
|
237
|
+
view_properties.add(child_prop.view_property)
|
|
238
|
+
|
|
239
|
+
return properties_by_views
|
|
165
240
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
241
|
+
@property
|
|
242
|
+
def logical_uri_by_view(self) -> dict[ViewEntity, URIRef]:
|
|
243
|
+
"""Get the logical URI by view."""
|
|
244
|
+
return {view.view: view.logical for view in self.dms.views if view.logical}
|
|
170
245
|
|
|
171
|
-
|
|
246
|
+
def logical_uri_by_property_by_view(
|
|
247
|
+
self,
|
|
248
|
+
include_ancestors: bool = False,
|
|
249
|
+
include_different_space: bool = False,
|
|
250
|
+
) -> dict[ViewEntity, dict[str, URIRef]]:
|
|
251
|
+
"""Get the logical URI by property by view."""
|
|
252
|
+
properties_by_view = self.properties_by_view(include_ancestors, include_different_space)
|
|
172
253
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
@classmethod
|
|
178
|
-
def _add_inherited_properties(
|
|
179
|
-
cls,
|
|
180
|
-
class_: T_ClassEntity,
|
|
181
|
-
class_property_pairs: dict[T_ClassEntity, list[T_Property]],
|
|
182
|
-
class_parent_pairs: dict[T_ClassEntity, list[T_ClassEntity]],
|
|
183
|
-
):
|
|
184
|
-
inheritance_path = get_inheritance_path(class_, class_parent_pairs)
|
|
185
|
-
for parent in inheritance_path:
|
|
186
|
-
# ParentClassEntity -> ClassEntity to match the type of class_property_pairs
|
|
187
|
-
if parent in class_property_pairs:
|
|
188
|
-
for property_ in class_property_pairs[parent]:
|
|
189
|
-
property_ = property_.model_copy() # type: ignore
|
|
190
|
-
|
|
191
|
-
# This corresponds to importing properties from parent class
|
|
192
|
-
# making sure that the property is attached to desired child class
|
|
193
|
-
cls._set_cls_entity(property_, class_)
|
|
194
|
-
|
|
195
|
-
# need same if we have RDF path to make sure that the starting class is the
|
|
196
|
-
if class_ in class_property_pairs:
|
|
197
|
-
class_property_pairs[class_].append(property_)
|
|
198
|
-
else:
|
|
199
|
-
class_property_pairs[class_] = [property_]
|
|
200
|
-
|
|
201
|
-
def class_property_pairs(
|
|
202
|
-
self, only_rdfpath: bool = False, consider_inheritance: bool = False
|
|
203
|
-
) -> dict[T_ClassEntity, dict[T_PropertyEntity, T_Property]]:
|
|
204
|
-
"""Returns a dictionary of classes with a dictionary of properties associated with them.
|
|
254
|
+
return {
|
|
255
|
+
view: {prop.view_property: prop.logical for prop in properties if prop.logical}
|
|
256
|
+
for view, properties in properties_by_view.items()
|
|
257
|
+
}
|
|
205
258
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
259
|
+
@property
|
|
260
|
+
def _class_by_neat_id(self) -> dict[URIRef, InformationClass]:
|
|
261
|
+
"""Get a dictionary of class neat IDs to
|
|
262
|
+
class entities."""
|
|
263
|
+
|
|
264
|
+
return {cls.neatId: cls for cls in self.information.classes if cls.neatId}
|
|
265
|
+
|
|
266
|
+
def class_by_suffix(self) -> dict[str, InformationClass]:
|
|
267
|
+
"""Get a dictionary of class suffixes to class entities."""
|
|
268
|
+
# TODO: Remove this method
|
|
269
|
+
class_dict: dict[str, InformationClass] = {}
|
|
270
|
+
for definition in self.information.classes:
|
|
271
|
+
entity = definition.class_
|
|
272
|
+
if entity.suffix in class_dict:
|
|
273
|
+
warnings.warn(
|
|
274
|
+
NeatValueWarning(
|
|
275
|
+
f"Class {entity} has been defined more than once! Only the first definition "
|
|
276
|
+
"will be considered, skipping the rest.."
|
|
277
|
+
),
|
|
278
|
+
stacklevel=2,
|
|
279
|
+
)
|
|
280
|
+
continue
|
|
281
|
+
class_dict[entity.suffix] = definition
|
|
282
|
+
return class_dict
|
|
209
283
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
While get_classes_with_properties returns a dictionary of classes with a list of
|
|
216
|
-
properties defined for them,
|
|
217
|
-
here we filter the properties based on the `only_rdfpath` parameter and only consider
|
|
218
|
-
the first definition of a property if it is defined more than once.
|
|
219
|
-
|
|
220
|
-
!!! note "only_rdfpath"
|
|
221
|
-
If only_rdfpath is True, only properties with RuleType.rdfpath will be returned as
|
|
222
|
-
a part of the dictionary of properties related to a class. Otherwise, all properties
|
|
223
|
-
will be returned.
|
|
224
|
-
|
|
225
|
-
!!! note "consider_inheritance"
|
|
226
|
-
If consider_inheritance is True, properties from parent classes will also be considered.
|
|
227
|
-
This means if a class has a parent class, and the parent class has properties defined for it,
|
|
228
|
-
while we do not have any properties defined for the child class, we will still consider the
|
|
229
|
-
properties from the parent class. If consider_inheritance is False, we will only consider
|
|
230
|
-
properties defined for the child class, thus if no properties are defined for the child class,
|
|
231
|
-
it will not be included in the returned dictionary.
|
|
232
|
-
"""
|
|
233
|
-
# TODO: https://cognitedata.atlassian.net/jira/software/projects/NEAT/boards/893?selectedIssue=NEAT-78
|
|
284
|
+
@property
|
|
285
|
+
def class_by_class_entity(self) -> dict[ClassEntity, InformationClass]:
|
|
286
|
+
"""Get a dictionary of class entities to class entities."""
|
|
287
|
+
rules = self.information
|
|
288
|
+
return {cls.class_: cls for cls in rules.classes}
|
|
234
289
|
|
|
235
|
-
|
|
290
|
+
@property
|
|
291
|
+
def view_by_view_entity(self) -> dict[ViewEntity, DMSView]:
|
|
292
|
+
"""Get a dictionary of class entities to class entities."""
|
|
293
|
+
rules = self.dms
|
|
294
|
+
return {view.view: view for view in rules.views}
|
|
295
|
+
|
|
296
|
+
def property_by_id(self) -> dict[str, list[InformationProperty]]:
|
|
297
|
+
"""Get a dictionary of property IDs to property entities."""
|
|
298
|
+
property_dict: dict[str, list[InformationProperty]] = defaultdict(list)
|
|
299
|
+
for prop in self.information.properties:
|
|
300
|
+
property_dict[prop.property_].append(prop)
|
|
301
|
+
return property_dict
|
|
236
302
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
303
|
+
def properties_by_id_by_class(
|
|
304
|
+
self,
|
|
305
|
+
has_instance_source: bool = False,
|
|
306
|
+
include_ancestors: bool = False,
|
|
307
|
+
) -> dict[ClassEntity, dict[str, InformationProperty]]:
|
|
308
|
+
"""Get a dictionary of class entities to dictionaries of property IDs to property entities."""
|
|
309
|
+
class_property_pairs: dict[ClassEntity, dict[str, InformationProperty]] = {}
|
|
310
|
+
for class_, properties in self.properties_by_class(include_ancestors).items():
|
|
311
|
+
processed_properties: dict[str, InformationProperty] = {}
|
|
312
|
+
for prop in properties:
|
|
313
|
+
if prop.property_ in processed_properties:
|
|
244
314
|
warnings.warn(
|
|
245
|
-
|
|
246
|
-
|
|
315
|
+
NeatValueWarning(
|
|
316
|
+
f"Property {processed_properties} for {class_} has been defined more than once!"
|
|
317
|
+
" Only the first definition will be considered, skipping the rest.."
|
|
318
|
+
),
|
|
247
319
|
stacklevel=2,
|
|
248
320
|
)
|
|
249
321
|
continue
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
and isinstance(property_, InformationProperty)
|
|
254
|
-
and isinstance(property_.instance_source, RDFPath)
|
|
255
|
-
) or not only_rdfpath:
|
|
256
|
-
processed_properties[prop_entity] = property_
|
|
322
|
+
if has_instance_source and prop.instance_source is None:
|
|
323
|
+
continue
|
|
324
|
+
processed_properties[prop.property_] = prop
|
|
257
325
|
class_property_pairs[class_] = processed_properties
|
|
258
326
|
|
|
259
327
|
return class_property_pairs
|
|
260
328
|
|
|
261
|
-
def
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
consider_inheritance: Whether to consider inheritance or not. Defaults False
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
|
|
269
|
-
"""
|
|
270
|
-
class_linkage = LinkageSet[T_ClassEntity, T_PropertyEntity]()
|
|
271
|
-
|
|
272
|
-
class_property_pairs = self.classes_with_properties(consider_inheritance)
|
|
273
|
-
properties = list(itertools.chain.from_iterable(class_property_pairs.values()))
|
|
274
|
-
|
|
275
|
-
for property_ in properties:
|
|
276
|
-
object_ = self._get_object(property_)
|
|
277
|
-
if object_ is not None:
|
|
278
|
-
class_linkage.add(
|
|
279
|
-
Linkage(
|
|
280
|
-
source_class=self._get_cls_entity(property_),
|
|
281
|
-
connecting_property=self._get_prop_entity(property_),
|
|
282
|
-
target_class=object_,
|
|
283
|
-
max_occurrence=self._get_max_occurrence(property_),
|
|
284
|
-
)
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
return class_linkage
|
|
329
|
+
def defined_views(self, include_ancestors: bool = False) -> set[ViewEntity]:
|
|
330
|
+
properties_by_view = self.properties_by_view(include_ancestors)
|
|
331
|
+
return {prop.view for prop in itertools.chain.from_iterable(properties_by_view.values())}
|
|
288
332
|
|
|
289
|
-
def
|
|
290
|
-
|
|
333
|
+
def defined_classes(
|
|
334
|
+
self,
|
|
335
|
+
include_ancestors: bool = False,
|
|
336
|
+
) -> set[ClassEntity]:
|
|
337
|
+
"""Returns classes that have properties defined for them in the data model.
|
|
291
338
|
|
|
292
339
|
Args:
|
|
293
|
-
|
|
340
|
+
include_ancestors: Whether to consider inheritance or not. Defaults False
|
|
294
341
|
|
|
295
342
|
Returns:
|
|
296
|
-
Set of classes that
|
|
343
|
+
Set of classes that have been defined in the data model
|
|
297
344
|
"""
|
|
298
|
-
|
|
299
|
-
return
|
|
345
|
+
properties_by_class = self.properties_by_class(include_ancestors)
|
|
346
|
+
return {prop.class_ for prop in itertools.chain.from_iterable(properties_by_class.values())}
|
|
300
347
|
|
|
301
|
-
def
|
|
302
|
-
|
|
348
|
+
def class_linkage(
|
|
349
|
+
self,
|
|
350
|
+
include_ancestors: bool = False,
|
|
351
|
+
) -> LinkageSet:
|
|
352
|
+
"""Returns a set of class linkages in the data model.
|
|
303
353
|
|
|
304
354
|
Args:
|
|
305
|
-
|
|
355
|
+
include_ancestors: Whether to consider inheritance or not. Defaults False
|
|
306
356
|
|
|
307
357
|
Returns:
|
|
308
|
-
Set of classes that have been defined in the data model
|
|
309
|
-
"""
|
|
310
|
-
class_property_pairs = self.classes_with_properties(consider_inheritance)
|
|
311
|
-
properties = list(itertools.chain.from_iterable(class_property_pairs.values()))
|
|
312
358
|
|
|
313
|
-
|
|
359
|
+
"""
|
|
360
|
+
class_linkage = LinkageSet()
|
|
314
361
|
|
|
315
|
-
|
|
316
|
-
"""Return a set of classes that are disconnected (i.e. isolated) from other classes.
|
|
362
|
+
properties_by_class = self.properties_by_class(include_ancestors)
|
|
317
363
|
|
|
318
|
-
|
|
319
|
-
|
|
364
|
+
prop: InformationProperty
|
|
365
|
+
for prop in itertools.chain.from_iterable(properties_by_class.values()):
|
|
366
|
+
if not isinstance(prop.value_type, ClassEntity):
|
|
367
|
+
continue
|
|
368
|
+
class_linkage.add(
|
|
369
|
+
Linkage(
|
|
370
|
+
source_class=prop.class_,
|
|
371
|
+
connecting_property=prop.property_,
|
|
372
|
+
target_class=prop.value_type,
|
|
373
|
+
max_occurrence=prop.max_count,
|
|
374
|
+
)
|
|
375
|
+
)
|
|
320
376
|
|
|
321
|
-
|
|
322
|
-
Set of classes that are disconnected from other classes
|
|
323
|
-
"""
|
|
324
|
-
return self.defined_classes(consider_inheritance) - self.connected_classes(consider_inheritance)
|
|
377
|
+
return class_linkage
|
|
325
378
|
|
|
326
379
|
def symmetrically_connected_classes(
|
|
327
|
-
self,
|
|
380
|
+
self,
|
|
381
|
+
include_ancestors: bool = False,
|
|
328
382
|
) -> set[tuple[ClassEntity, ClassEntity]]:
|
|
329
383
|
"""Returns a set of pairs of symmetrically linked classes.
|
|
330
384
|
|
|
331
385
|
Args:
|
|
332
|
-
|
|
386
|
+
include_ancestors: Whether to consider inheritance or not. Defaults False
|
|
333
387
|
|
|
334
388
|
Returns:
|
|
335
389
|
Set of pairs of symmetrically linked classes
|
|
@@ -339,11 +393,8 @@ class BaseAnalysis(ABC, Generic[T_Rules, T_Class, T_Property, T_ClassEntity, T_P
|
|
|
339
393
|
in both directions. For example, if class A is connected to class B, and class B
|
|
340
394
|
is connected to class A, then classes A and B are symmetrically connected.
|
|
341
395
|
"""
|
|
342
|
-
|
|
343
|
-
# TODO: Find better name for this method
|
|
344
396
|
sym_pairs: set[tuple[ClassEntity, ClassEntity]] = set()
|
|
345
|
-
|
|
346
|
-
class_linkage = self.class_linkage(consider_inheritance)
|
|
397
|
+
class_linkage = self.class_linkage(include_ancestors)
|
|
347
398
|
if not class_linkage:
|
|
348
399
|
return sym_pairs
|
|
349
400
|
|
|
@@ -356,30 +407,171 @@ class BaseAnalysis(ABC, Generic[T_Rules, T_Class, T_Property, T_ClassEntity, T_P
|
|
|
356
407
|
sym_pairs.add((source, target))
|
|
357
408
|
return sym_pairs
|
|
358
409
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
) -> dict[T_PropertyEntity, list[T_Property]]:
|
|
362
|
-
"""This is used to capture all definitions of a property in the data model."""
|
|
363
|
-
property_dict: dict[T_PropertyEntity, list[T_Property]] = defaultdict(list)
|
|
364
|
-
for definition in self._get_properties():
|
|
365
|
-
property_dict[self._get_prop_entity(definition)].append(definition)
|
|
366
|
-
return property_dict
|
|
410
|
+
@overload
|
|
411
|
+
def _properties_by_neat_id(self, format: Literal["info"] = "info") -> dict[URIRef, InformationProperty]: ...
|
|
367
412
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
413
|
+
@overload
|
|
414
|
+
def _properties_by_neat_id(self, format: Literal["dms"] = "dms") -> dict[URIRef, DMSProperty]: ...
|
|
415
|
+
|
|
416
|
+
def _properties_by_neat_id(
|
|
417
|
+
self, format: Literal["info", "dms"] = "info"
|
|
418
|
+
) -> dict[URIRef, InformationProperty] | dict[URIRef, DMSProperty]:
|
|
419
|
+
if format == "info":
|
|
420
|
+
return {prop.neatId: prop for prop in self.information.properties if prop.neatId}
|
|
421
|
+
elif format == "dms":
|
|
422
|
+
return {prop.neatId: prop for prop in self.dms.properties if prop.neatId}
|
|
423
|
+
else:
|
|
424
|
+
raise NeatValueError(f"Invalid format: {format}")
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def classes_by_neat_id(self) -> dict[URIRef, InformationClass]:
|
|
428
|
+
return {class_.neatId: class_ for class_ in self.information.classes if class_.neatId}
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def multi_value_properties(self) -> list[InformationProperty]:
|
|
432
|
+
return [prop_ for prop_ in self.information.properties if isinstance(prop_.value_type, MultiValueTypeInfo)]
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def view_query_by_id(
|
|
436
|
+
self,
|
|
437
|
+
) -> "ViewQueryDict":
|
|
438
|
+
# Trigger error if any of these are missing
|
|
439
|
+
_ = self.information
|
|
440
|
+
_ = self.dms
|
|
441
|
+
|
|
442
|
+
# caching results for faster access
|
|
443
|
+
classes_by_neat_id = self._class_by_neat_id
|
|
444
|
+
properties_by_class = self.properties_by_class(include_ancestors=True)
|
|
445
|
+
logical_uri_by_view = self.logical_uri_by_view
|
|
446
|
+
logical_uri_by_property_by_view = self.logical_uri_by_property_by_view(include_ancestors=True)
|
|
447
|
+
information_properties_by_neat_id = self._properties_by_neat_id()
|
|
448
|
+
|
|
449
|
+
query_configs = ViewQueryDict()
|
|
450
|
+
for view in self.dms.views:
|
|
451
|
+
# this entire block of sequential if statements checks:
|
|
452
|
+
# 1. connection of dms to info rules
|
|
453
|
+
# 2. correct paring of information and dms rules
|
|
454
|
+
# 3. connection of info rules to instances
|
|
455
|
+
if (
|
|
456
|
+
(neat_id := logical_uri_by_view.get(view.view))
|
|
457
|
+
and (class_ := classes_by_neat_id.get(neat_id))
|
|
458
|
+
and (uri := class_.instance_source)
|
|
459
|
+
):
|
|
460
|
+
view_query = ViewQuery(
|
|
461
|
+
view_id=view.view.as_id(),
|
|
462
|
+
rdf_type=uri,
|
|
463
|
+
# start off with renaming of properties on the information level
|
|
464
|
+
# this is to encounter for special cases of e.g. space, startNode and endNode
|
|
465
|
+
property_renaming_config=(
|
|
466
|
+
{uri: prop_.property_ for prop_ in info_properties for uri in prop_.instance_source or []}
|
|
467
|
+
if (info_properties := properties_by_class.get(class_.class_))
|
|
468
|
+
else {}
|
|
469
|
+
),
|
|
378
470
|
)
|
|
471
|
+
|
|
472
|
+
if logical_uri_by_property := logical_uri_by_property_by_view.get(view.view):
|
|
473
|
+
for target_name, neat_id in logical_uri_by_property.items():
|
|
474
|
+
if (property_ := information_properties_by_neat_id.get(neat_id)) and (
|
|
475
|
+
uris := property_.instance_source
|
|
476
|
+
):
|
|
477
|
+
for uri in uris:
|
|
478
|
+
view_query.property_renaming_config[uri] = target_name
|
|
479
|
+
|
|
480
|
+
query_configs[view.view.as_id()] = view_query
|
|
481
|
+
|
|
482
|
+
return query_configs
|
|
483
|
+
|
|
484
|
+
def _dms_di_graph(self, format: Literal["data-model", "implements"] = "data-model") -> nx.DiGraph:
|
|
485
|
+
"""Generate a DiGraph from the DMS rules."""
|
|
486
|
+
di_graph = nx.DiGraph()
|
|
487
|
+
|
|
488
|
+
rules = self.dms
|
|
489
|
+
|
|
490
|
+
# Views with properties or used as ValueType
|
|
491
|
+
# If a view is not used in properties or as ValueType, it is not added to the graph
|
|
492
|
+
# as we typically do not have the properties for it.
|
|
493
|
+
used_views = {prop_.view for prop_ in rules.properties} | {
|
|
494
|
+
prop_.value_type for prop_ in rules.properties if isinstance(prop_.value_type, ViewEntity)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
# Add nodes and edges from Views sheet
|
|
498
|
+
for view in rules.views:
|
|
499
|
+
if view.view not in used_views:
|
|
379
500
|
continue
|
|
380
|
-
|
|
381
|
-
|
|
501
|
+
# if possible use human-readable label coming from the view name
|
|
502
|
+
if not di_graph.has_node(view.view.suffix):
|
|
503
|
+
di_graph.add_node(view.view.suffix, label=view.view.suffix)
|
|
504
|
+
|
|
505
|
+
if format == "implements" and view.implements:
|
|
506
|
+
for implement in view.implements:
|
|
507
|
+
if not di_graph.has_node(implement.suffix):
|
|
508
|
+
di_graph.add_node(implement.suffix, label=implement.suffix)
|
|
509
|
+
|
|
510
|
+
di_graph.add_edge(
|
|
511
|
+
view.view.suffix,
|
|
512
|
+
implement.suffix,
|
|
513
|
+
label="implements",
|
|
514
|
+
dashes=True,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if format == "data-model":
|
|
518
|
+
# Add nodes and edges from Properties sheet
|
|
519
|
+
for prop_ in rules.properties:
|
|
520
|
+
if prop_.connection and isinstance(prop_.value_type, ViewEntity):
|
|
521
|
+
if not di_graph.has_node(prop_.view.suffix):
|
|
522
|
+
di_graph.add_node(prop_.view.suffix, label=prop_.view.suffix)
|
|
523
|
+
|
|
524
|
+
if not di_graph.has_node(prop_.value_type.suffix):
|
|
525
|
+
di_graph.add_node(prop_.value_type.suffix, label=prop_.value_type.suffix)
|
|
526
|
+
|
|
527
|
+
di_graph.add_edge(
|
|
528
|
+
prop_.view.suffix,
|
|
529
|
+
prop_.value_type.suffix,
|
|
530
|
+
label=prop_.name or prop_.view_property,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return di_graph
|
|
534
|
+
|
|
535
|
+
def _info_di_graph(self, format: Literal["data-model", "implements"] = "data-model") -> nx.DiGraph:
|
|
536
|
+
"""Generate DiGraph representing information data model."""
|
|
537
|
+
|
|
538
|
+
rules = self.information
|
|
539
|
+
di_graph = nx.DiGraph()
|
|
540
|
+
|
|
541
|
+
# Add nodes and edges from Views sheet
|
|
542
|
+
for class_ in rules.classes:
|
|
543
|
+
# if possible use human readable label coming from the view name
|
|
544
|
+
if not di_graph.has_node(class_.class_.suffix):
|
|
545
|
+
di_graph.add_node(
|
|
546
|
+
class_.class_.suffix,
|
|
547
|
+
label=class_.name or class_.class_.suffix,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if format == "implements" and class_.implements:
|
|
551
|
+
for parent in class_.implements:
|
|
552
|
+
if not di_graph.has_node(parent.suffix):
|
|
553
|
+
di_graph.add_node(parent.suffix, label=parent.suffix)
|
|
554
|
+
di_graph.add_edge(
|
|
555
|
+
class_.class_.suffix,
|
|
556
|
+
parent.suffix,
|
|
557
|
+
label="implements",
|
|
558
|
+
dashes=True,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
if format == "data-model":
|
|
562
|
+
# Add nodes and edges from Properties sheet
|
|
563
|
+
for prop_ in rules.properties:
|
|
564
|
+
if isinstance(prop_.value_type, ClassEntity) and not isinstance(prop_.value_type, UnknownEntity):
|
|
565
|
+
if not di_graph.has_node(prop_.class_.suffix):
|
|
566
|
+
di_graph.add_node(prop_.class_.suffix, label=prop_.class_.suffix)
|
|
567
|
+
|
|
568
|
+
if not di_graph.has_node(prop_.value_type.suffix):
|
|
569
|
+
di_graph.add_node(prop_.value_type.suffix, label=prop_.value_type.suffix)
|
|
570
|
+
|
|
571
|
+
di_graph.add_edge(
|
|
572
|
+
prop_.class_.suffix,
|
|
573
|
+
prop_.value_type.suffix,
|
|
574
|
+
label=prop_.name or prop_.property_,
|
|
575
|
+
)
|
|
382
576
|
|
|
383
|
-
|
|
384
|
-
def subset_rules(self, desired_classes: set[T_ClassEntity]) -> T_Rules:
|
|
385
|
-
raise NotImplementedError
|
|
577
|
+
return di_graph
|