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.

Files changed (67) hide show
  1. cognite/neat/_alpha.py +2 -0
  2. cognite/neat/_client/_api/schema.py +17 -1
  3. cognite/neat/_client/data_classes/schema.py +3 -3
  4. cognite/neat/_constants.py +11 -0
  5. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +9 -10
  6. cognite/neat/_graph/extractors/_iodd.py +3 -3
  7. cognite/neat/_graph/extractors/_mock_graph_generator.py +9 -7
  8. cognite/neat/_graph/loaders/_rdf2dms.py +285 -346
  9. cognite/neat/_graph/queries/_base.py +28 -92
  10. cognite/neat/_graph/transformers/__init__.py +1 -3
  11. cognite/neat/_graph/transformers/_rdfpath.py +2 -49
  12. cognite/neat/_issues/__init__.py +1 -6
  13. cognite/neat/_issues/_base.py +21 -252
  14. cognite/neat/_issues/_contextmanagers.py +46 -0
  15. cognite/neat/_issues/_factory.py +61 -0
  16. cognite/neat/_issues/errors/__init__.py +18 -4
  17. cognite/neat/_issues/errors/_wrapper.py +81 -3
  18. cognite/neat/_issues/formatters.py +4 -4
  19. cognite/neat/_issues/warnings/__init__.py +3 -2
  20. cognite/neat/_issues/warnings/_properties.py +8 -0
  21. cognite/neat/_rules/_constants.py +9 -0
  22. cognite/neat/_rules/_shared.py +3 -2
  23. cognite/neat/_rules/analysis/__init__.py +2 -3
  24. cognite/neat/_rules/analysis/_base.py +450 -258
  25. cognite/neat/_rules/catalog/info-rules-imf.xlsx +0 -0
  26. cognite/neat/_rules/exporters/_rules2excel.py +2 -8
  27. cognite/neat/_rules/exporters/_rules2instance_template.py +2 -2
  28. cognite/neat/_rules/exporters/_rules2ontology.py +5 -4
  29. cognite/neat/_rules/importers/_base.py +2 -47
  30. cognite/neat/_rules/importers/_dms2rules.py +7 -10
  31. cognite/neat/_rules/importers/_dtdl2rules/dtdl_importer.py +2 -2
  32. cognite/neat/_rules/importers/_rdf/_inference2rules.py +59 -25
  33. cognite/neat/_rules/importers/_rdf/_shared.py +1 -1
  34. cognite/neat/_rules/importers/_spreadsheet2rules.py +12 -9
  35. cognite/neat/_rules/models/dms/_rules.py +3 -1
  36. cognite/neat/_rules/models/dms/_rules_input.py +4 -0
  37. cognite/neat/_rules/models/dms/_validation.py +14 -4
  38. cognite/neat/_rules/models/entities/_loaders.py +1 -1
  39. cognite/neat/_rules/models/entities/_multi_value.py +2 -2
  40. cognite/neat/_rules/models/information/_rules.py +18 -17
  41. cognite/neat/_rules/models/information/_rules_input.py +2 -1
  42. cognite/neat/_rules/models/information/_validation.py +3 -1
  43. cognite/neat/_rules/transformers/__init__.py +8 -2
  44. cognite/neat/_rules/transformers/_converters.py +242 -43
  45. cognite/neat/_rules/transformers/_verification.py +5 -10
  46. cognite/neat/_session/_base.py +4 -4
  47. cognite/neat/_session/_prepare.py +12 -0
  48. cognite/neat/_session/_read.py +21 -17
  49. cognite/neat/_session/_show.py +11 -123
  50. cognite/neat/_session/_state.py +0 -2
  51. cognite/neat/_session/_subset.py +64 -0
  52. cognite/neat/_session/_to.py +63 -12
  53. cognite/neat/_store/_graph_store.py +5 -246
  54. cognite/neat/_utils/rdf_.py +2 -2
  55. cognite/neat/_utils/spreadsheet.py +44 -1
  56. cognite/neat/_utils/text.py +51 -32
  57. cognite/neat/_version.py +1 -1
  58. {cognite_neat-0.109.3.dist-info → cognite_neat-0.110.0.dist-info}/METADATA +1 -1
  59. {cognite_neat-0.109.3.dist-info → cognite_neat-0.110.0.dist-info}/RECORD +62 -64
  60. {cognite_neat-0.109.3.dist-info → cognite_neat-0.110.0.dist-info}/WHEEL +1 -1
  61. cognite/neat/_graph/queries/_construct.py +0 -187
  62. cognite/neat/_graph/queries/_shared.py +0 -173
  63. cognite/neat/_rules/analysis/_dms.py +0 -57
  64. cognite/neat/_rules/analysis/_information.py +0 -249
  65. cognite/neat/_rules/models/_rdfpath.py +0 -372
  66. {cognite_neat-0.109.3.dist-info → cognite_neat-0.110.0.dist-info}/LICENSE +0 -0
  67. {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 typing import Generic, TypeVar, cast
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._rules.models._base_rules import BaseRules
13
- from cognite.neat._rules.models._rdfpath import RDFPath
14
- from cognite.neat._rules.models.dms._rules import DMSProperty, DMSView
15
- from cognite.neat._rules.models.entities import (
16
- ClassEntity,
17
- Entity,
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
- T_Rules = TypeVar("T_Rules", bound=BaseRules)
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(Generic[T_ClassEntity, T_PropertyEntity]):
32
- source_class: T_ClassEntity
33
- connecting_property: T_PropertyEntity
34
- target_class: T_ClassEntity
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, Generic[T_ClassEntity, T_PropertyEntity], Set[Linkage[T_ClassEntity, T_PropertyEntity]]):
36
+ class LinkageSet(set, Set[Linkage]):
39
37
  @property
40
- def source_class(self) -> set[T_ClassEntity]:
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[T_ClassEntity]:
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[T_ClassEntity, set[T_ClassEntity]]:
48
- target_classes_by_source: dict[T_ClassEntity, set[T_ClassEntity]] = defaultdict(set)
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
- class BaseAnalysis(ABC, Generic[T_Rules, T_Class, T_Property, T_ClassEntity, T_PropertyEntity]):
69
- def __init__(self, rules: T_Rules) -> None:
70
- self.rules = rules
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
- @abstractmethod
77
- def _get_properties(self) -> list[T_Property]:
78
- raise NotImplementedError
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
- @abstractmethod
81
- def _get_cls_entity(self, class_: T_Class | T_Property) -> T_ClassEntity:
82
- raise NotImplementedError
78
+ def __setitem__(self, k: dm.ViewId, v: ViewQuery) -> None:
79
+ super().__setitem__(k, v)
83
80
 
84
- @abstractmethod
85
- def _get_prop_entity(self, property_: T_Property) -> T_PropertyEntity:
86
- raise NotImplementedError
81
+ def __delitem__(self, k: dm.ViewId) -> None:
82
+ super().__delitem__(k)
87
83
 
88
- @abstractmethod
89
- def _get_cls_parents(self, class_: T_Class) -> list[T_ClassEntity] | None:
90
- raise NotImplementedError
84
+ def __iter__(self) -> Iterator[dm.ViewId]:
85
+ return super().__iter__()
91
86
 
92
- @classmethod
93
- @abstractmethod
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
- @abstractmethod
98
- def _get_object(self, property_: T_Property) -> T_ClassEntity | None:
99
- raise NotImplementedError
90
+ def values(self) -> ValuesView[ViewQuery]: # type: ignore[override]
91
+ return super().values()
100
92
 
101
- @abstractmethod
102
- def _get_max_occurrence(self, property_: T_Property) -> int | float | None:
103
- raise NotImplementedError
93
+ def items(self) -> ItemsView[dm.ViewId, ViewQuery]: # type: ignore[override]
94
+ return super().items()
104
95
 
105
- @property
106
- def directly_referred_classes(self) -> set[ClassEntity]:
107
- raise NotImplementedError
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
- @property
110
- def inherited_referred_classes(self) -> set[ClassEntity]:
111
- raise NotImplementedError
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 properties_by_neat_id(self) -> dict[URIRef, T_Property]:
115
- return {cast(URIRef, prop.neatId): prop for prop in self._get_properties()}
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 classes_by_neat_id(self) -> dict[URIRef, T_Class]:
119
- return {cast(URIRef, class_.neatId): class_ for class_ in self._get_classes()}
120
-
121
- # Todo Lru cache this method.
122
- def class_parent_pairs(self, allow_different_space: bool = False) -> dict[T_ClassEntity, list[T_ClassEntity]]:
123
- """This only returns class - parent pairs only if parent is in the same data model"""
124
- class_subclass_pairs: dict[T_ClassEntity, list[T_ClassEntity]] = {}
125
- for cls_ in self._get_classes():
126
- entity = self._get_cls_entity(cls_)
127
- class_subclass_pairs[entity] = []
128
- for parent in self._get_cls_parents(cls_) or []:
129
- if parent.prefix == entity.prefix or allow_different_space:
130
- class_subclass_pairs[entity].append(parent)
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
- f"Parent class {parent} of class {cls_} is not in the same namespace, skipping !",
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
- return class_subclass_pairs
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 classes_with_properties(
140
- self, consider_inheritance: bool = False, allow_different_namespace: bool = False
141
- ) -> dict[T_ClassEntity, list[T_Property]]:
142
- """Returns classes that have been defined in the data model.
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
- consider_inheritance: Whether to consider inheritance or not. Defaults False
146
- allow_different_namespace: When considering inheritance, whether to allow parents from
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
- Dictionary of classes with a list of properties defined for them
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
- class_property_pairs: dict[T_ClassEntity, list[T_Property]] = defaultdict(list)
162
-
163
- for property_ in self._get_properties():
164
- class_property_pairs[self._get_cls_entity(property_)].append(property_) # type: ignore
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
- if consider_inheritance:
167
- class_parent_pairs = self.class_parent_pairs(allow_different_namespace)
168
- for class_ in class_parent_pairs:
169
- self._add_inherited_properties(class_, class_property_pairs, class_parent_pairs)
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
- return class_property_pairs
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
- def class_inheritance_path(self, class_: ClassEntity) -> list[ClassEntity]:
174
- class_parent_pairs = self.class_parent_pairs()
175
- return get_inheritance_path(class_, class_parent_pairs)
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
- Args:
207
- only_rdfpath : To consider only properties which have rule `rdfpath` set. Defaults False
208
- consider_inheritance: Whether to consider inheritance or not. Defaults False
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
- Returns:
211
- Dictionary of classes with a dictionary of properties associated with them.
212
-
213
- !!! note "difference to get_classes_with_properties"
214
- This method returns a dictionary of classes with a dictionary of properties associated with them.
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
- class_property_pairs: dict[T_ClassEntity, dict[T_PropertyEntity, T_Property]] = {}
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
- for class_, properties in self.classes_with_properties(consider_inheritance).items():
238
- processed_properties: dict[T_PropertyEntity, T_Property] = {}
239
- for property_ in properties:
240
- prop_entity = self._get_prop_entity(property_)
241
- if prop_entity in processed_properties:
242
- # TODO: use appropriate Warning class from _exceptions.py
243
- # if missing make one !
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
- f"Property {processed_properties} for {class_} has been defined more than once!"
246
- " Only the first definition will be considered, skipping the rest..",
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
- if (
252
- only_rdfpath
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 class_linkage(self, consider_inheritance: bool = False) -> LinkageSet[T_ClassEntity, T_PropertyEntity]:
262
- """Returns a set of class linkages in the data model.
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 connected_classes(self, consider_inheritance: bool = False) -> set[T_ClassEntity]:
290
- """Return a set of classes that are connected to other classes.
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
- consider_inheritance: Whether to consider inheritance or not. Defaults False
340
+ include_ancestors: Whether to consider inheritance or not. Defaults False
294
341
 
295
342
  Returns:
296
- Set of classes that are connected to other classes
343
+ Set of classes that have been defined in the data model
297
344
  """
298
- class_linkage = self.class_linkage(consider_inheritance)
299
- return class_linkage.source_class.union(class_linkage.target_class)
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 defined_classes(self, consider_inheritance: bool = False) -> set[T_ClassEntity]:
302
- """Returns classes that have properties defined for them in the data model.
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
- consider_inheritance: Whether to consider inheritance or not. Defaults False
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
- return {self._get_cls_entity(property) for property in properties}
359
+ """
360
+ class_linkage = LinkageSet()
314
361
 
315
- def disconnected_classes(self, consider_inheritance: bool = False) -> set[T_ClassEntity]:
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
- Args:
319
- consider_inheritance: Whether to consider inheritance or not. Defaults False
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
- Returns:
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, consider_inheritance: bool = False
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
- consider_inheritance: Whether to consider inheritance or not. Defaults False
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
- def as_property_dict(
360
- self,
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
- def as_class_dict(self) -> dict[str, T_Class]:
369
- """This is to simplify access to classes through dict."""
370
- class_dict: dict[str, T_Class] = {}
371
- for definition in self._get_classes():
372
- entity = self._get_cls_entity(definition)
373
- if entity.suffix in class_dict:
374
- warnings.warn(
375
- f"Class {entity} has been defined more than once! Only the first definition "
376
- "will be considered, skipping the rest..",
377
- stacklevel=2,
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
- class_dict[entity.suffix] = definition
381
- return class_dict
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
- @abstractmethod
384
- def subset_rules(self, desired_classes: set[T_ClassEntity]) -> T_Rules:
385
- raise NotImplementedError
577
+ return di_graph