cognite-neat 0.121.0__py3-none-any.whl → 0.121.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cognite-neat might be problematic. Click here for more details.
- cognite/neat/_version.py +1 -1
- cognite/neat/core/_client/_api/statistics.py +91 -0
- cognite/neat/core/_client/_api_client.py +2 -0
- cognite/neat/core/_client/data_classes/statistics.py +125 -0
- cognite/neat/core/_client/testing.py +4 -0
- cognite/neat/core/_constants.py +6 -7
- cognite/neat/core/{_rules → _data_model}/_constants.py +25 -18
- cognite/neat/core/_data_model/_shared.py +59 -0
- cognite/neat/core/_data_model/analysis/__init__.py +3 -0
- cognite/neat/core/{_rules → _data_model}/analysis/_base.py +202 -195
- cognite/neat/core/{_rules → _data_model}/catalog/__init__.py +1 -1
- cognite/neat/core/{_rules → _data_model}/exporters/__init__.py +5 -5
- cognite/neat/core/{_rules → _data_model}/exporters/_base.py +10 -8
- cognite/neat/core/{_rules/exporters/_rules2dms.py → _data_model/exporters/_data_model2dms.py} +22 -18
- cognite/neat/core/{_rules/exporters/_rules2excel.py → _data_model/exporters/_data_model2excel.py} +61 -56
- cognite/neat/core/{_rules/exporters/_rules2instance_template.py → _data_model/exporters/_data_model2instance_template.py} +11 -9
- cognite/neat/core/{_rules/exporters/_rules2ontology.py → _data_model/exporters/_data_model2ontology.py} +64 -61
- cognite/neat/core/{_rules/exporters/_rules2yaml.py → _data_model/exporters/_data_model2yaml.py} +21 -18
- cognite/neat/core/{_rules → _data_model}/importers/__init__.py +6 -8
- cognite/neat/core/{_rules → _data_model}/importers/_base.py +8 -6
- cognite/neat/core/_data_model/importers/_base_file_reader.py +56 -0
- cognite/neat/core/{_rules/importers/_yaml2rules.py → _data_model/importers/_dict2data_model.py} +41 -21
- cognite/neat/core/{_rules/importers/_dms2rules.py → _data_model/importers/_dms2data_model.py} +79 -66
- cognite/neat/core/{_rules/importers/_dtdl2rules → _data_model/importers/_dtdl2data_model}/dtdl_converter.py +41 -41
- cognite/neat/core/{_rules/importers/_dtdl2rules → _data_model/importers/_dtdl2data_model}/dtdl_importer.py +16 -16
- cognite/neat/core/{_rules/importers/_dtdl2rules → _data_model/importers/_dtdl2data_model}/spec.py +3 -3
- cognite/neat/core/{_rules → _data_model}/importers/_rdf/_base.py +18 -16
- cognite/neat/core/{_rules → _data_model}/importers/_rdf/_imf2rules.py +17 -17
- cognite/neat/core/{_rules → _data_model}/importers/_rdf/_inference2rules.py +50 -50
- cognite/neat/core/{_rules → _data_model}/importers/_rdf/_owl2rules.py +14 -14
- cognite/neat/core/{_rules → _data_model}/importers/_rdf/_shared.py +25 -25
- cognite/neat/core/{_rules/importers/_spreadsheet2rules.py → _data_model/importers/_spreadsheet2data_model.py} +69 -38
- cognite/neat/core/_data_model/models/__init__.py +36 -0
- cognite/neat/core/{_rules/models/_base_input.py → _data_model/models/_base_unverified.py} +12 -12
- cognite/neat/core/{_rules/models/_base_rules.py → _data_model/models/_base_verified.py} +13 -13
- cognite/neat/core/{_rules → _data_model}/models/_types.py +13 -13
- cognite/neat/core/_data_model/models/conceptual/__init__.py +25 -0
- cognite/neat/core/{_rules/models/information/_rules_input.py → _data_model/models/conceptual/_unverified.py} +46 -43
- cognite/neat/core/{_rules/models/information → _data_model/models/conceptual}/_validation.py +93 -79
- cognite/neat/core/{_rules/models/information/_rules.py → _data_model/models/conceptual/_verified.py} +83 -83
- cognite/neat/core/{_rules → _data_model}/models/data_types.py +4 -4
- cognite/neat/core/{_rules → _data_model}/models/entities/__init__.py +8 -8
- cognite/neat/core/{_rules → _data_model}/models/entities/_loaders.py +12 -11
- cognite/neat/core/{_rules → _data_model}/models/entities/_multi_value.py +7 -7
- cognite/neat/core/{_rules → _data_model}/models/entities/_single_value.py +45 -39
- cognite/neat/core/{_rules → _data_model}/models/entities/_types.py +9 -3
- cognite/neat/core/{_rules → _data_model}/models/entities/_wrapped.py +3 -3
- cognite/neat/core/{_rules → _data_model}/models/mapping/_classic2core.py +12 -9
- cognite/neat/core/_data_model/models/physical/__init__.py +40 -0
- cognite/neat/core/{_rules/models/dms → _data_model/models/physical}/_exporter.py +83 -64
- cognite/neat/core/{_rules/models/dms/_rules_input.py → _data_model/models/physical/_unverified.py} +56 -44
- cognite/neat/core/{_rules/models/dms → _data_model/models/physical}/_validation.py +20 -17
- cognite/neat/core/{_rules/models/dms/_rules.py → _data_model/models/physical/_verified.py} +79 -71
- cognite/neat/core/{_rules → _data_model}/transformers/__init__.py +27 -23
- cognite/neat/core/{_rules → _data_model}/transformers/_base.py +29 -19
- cognite/neat/core/{_rules → _data_model}/transformers/_converters.py +758 -659
- cognite/neat/core/{_rules → _data_model}/transformers/_mapping.py +79 -60
- cognite/neat/core/_data_model/transformers/_verification.py +120 -0
- cognite/neat/core/{_graph → _instances}/extractors/_base.py +2 -2
- cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_base.py +1 -1
- cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_classic.py +17 -11
- cognite/neat/core/{_graph → _instances}/extractors/_dms_graph.py +47 -39
- cognite/neat/core/{_graph → _instances}/extractors/_mock_graph_generator.py +102 -99
- cognite/neat/core/{_graph → _instances}/extractors/_rdf_file.py +2 -2
- cognite/neat/core/{_graph → _instances}/loaders/_base.py +2 -2
- cognite/neat/core/{_graph → _instances}/loaders/_rdf2dms.py +16 -14
- cognite/neat/core/{_graph → _instances}/transformers/_base.py +7 -4
- cognite/neat/core/{_graph → _instances}/transformers/_classic_cdf.py +1 -1
- cognite/neat/core/{_graph → _instances}/transformers/_value_type.py +2 -6
- cognite/neat/core/_issues/_base.py +4 -4
- cognite/neat/core/_issues/errors/__init__.py +2 -2
- cognite/neat/core/_issues/errors/_wrapper.py +2 -2
- cognite/neat/core/_issues/warnings/__init__.py +2 -0
- cognite/neat/core/_issues/warnings/_models.py +4 -4
- cognite/neat/core/_issues/warnings/_properties.py +7 -0
- cognite/neat/core/_store/__init__.py +3 -3
- cognite/neat/core/_store/{_rules_store.py → _data_model.py} +128 -121
- cognite/neat/core/_store/{_graph_store.py → _instance.py} +7 -8
- cognite/neat/core/_store/_provenance.py +2 -2
- cognite/neat/core/_store/exceptions.py +4 -4
- cognite/neat/core/_utils/rdf_.py +14 -0
- cognite/neat/core/_utils/spreadsheet.py +1 -1
- cognite/neat/core/_utils/text.py +2 -2
- cognite/neat/session/_base.py +29 -25
- cognite/neat/session/_drop.py +3 -3
- cognite/neat/session/_fix.py +2 -2
- cognite/neat/session/_inspect.py +5 -5
- cognite/neat/session/_mapping.py +11 -9
- cognite/neat/session/_prepare.py +4 -4
- cognite/neat/session/_read.py +15 -15
- cognite/neat/session/_set.py +5 -5
- cognite/neat/session/_show.py +11 -11
- cognite/neat/session/_state.py +17 -17
- cognite/neat/session/_subset.py +14 -11
- cognite/neat/session/_template.py +19 -19
- cognite/neat/session/_to.py +21 -21
- cognite/neat/session/_wizard.py +1 -1
- {cognite_neat-0.121.0.dist-info → cognite_neat-0.121.2.dist-info}/METADATA +1 -1
- cognite_neat-0.121.2.dist-info/RECORD +189 -0
- cognite/neat/core/_rules/_shared.py +0 -43
- cognite/neat/core/_rules/analysis/__init__.py +0 -3
- cognite/neat/core/_rules/exporters/_validation.py +0 -14
- cognite/neat/core/_rules/models/__init__.py +0 -34
- cognite/neat/core/_rules/models/dms/__init__.py +0 -32
- cognite/neat/core/_rules/models/information/__init__.py +0 -20
- cognite/neat/core/_rules/transformers/_verification.py +0 -111
- cognite_neat-0.121.0.dist-info/RECORD +0 -187
- /cognite/neat/core/{_graph → _data_model}/__init__.py +0 -0
- /cognite/neat/core/{_rules → _data_model}/catalog/classic_model.xlsx +0 -0
- /cognite/neat/core/{_rules/catalog/info-rules-imf.xlsx → _data_model/catalog/conceptual-imf-data-model.xlsx} +0 -0
- /cognite/neat/core/{_rules → _data_model}/catalog/hello_world_pump.xlsx +0 -0
- /cognite/neat/core/{_rules/importers/_dtdl2rules → _data_model/importers/_dtdl2data_model}/__init__.py +0 -0
- /cognite/neat/core/{_rules/importers/_dtdl2rules → _data_model/importers/_dtdl2data_model}/_unit_lookup.py +0 -0
- /cognite/neat/core/{_rules → _data_model}/importers/_rdf/__init__.py +0 -0
- /cognite/neat/core/{_rules → _data_model}/models/entities/_constants.py +0 -0
- /cognite/neat/core/{_rules → _data_model}/models/mapping/__init__.py +0 -0
- /cognite/neat/core/{_rules → _data_model}/models/mapping/_classic2core.yaml +0 -0
- /cognite/neat/core/{_graph/extractors/_classic_cdf → _instances}/__init__.py +0 -0
- /cognite/neat/core/{_graph → _instances}/_shared.py +0 -0
- /cognite/neat/core/{_graph → _instances}/_tracking/__init__.py +0 -0
- /cognite/neat/core/{_graph → _instances}/_tracking/base.py +0 -0
- /cognite/neat/core/{_graph → _instances}/_tracking/log.py +0 -0
- /cognite/neat/core/{_graph → _instances}/examples/Knowledge-Graph-Nordic44-dirty.xml +0 -0
- /cognite/neat/core/{_graph → _instances}/examples/Knowledge-Graph-Nordic44.xml +0 -0
- /cognite/neat/core/{_graph → _instances}/examples/__init__.py +0 -0
- /cognite/neat/core/{_graph → _instances}/examples/skos-capturing-sheet-wind-topics.xlsx +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/__init__.py +0 -0
- /cognite/neat/core/{_rules → _instances/extractors/_classic_cdf}/__init__.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_assets.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_data_sets.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_events.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_files.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_labels.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_relationships.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_sequences.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_classic_cdf/_timeseries.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_dict.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_dms.py +0 -0
- /cognite/neat/core/{_graph → _instances}/extractors/_raw.py +0 -0
- /cognite/neat/core/{_graph → _instances}/loaders/__init__.py +0 -0
- /cognite/neat/core/{_graph → _instances}/queries/__init__.py +0 -0
- /cognite/neat/core/{_graph → _instances}/queries/_base.py +0 -0
- /cognite/neat/core/{_graph → _instances}/queries/_queries.py +0 -0
- /cognite/neat/core/{_graph → _instances}/queries/_select.py +0 -0
- /cognite/neat/core/{_graph → _instances}/queries/_update.py +0 -0
- /cognite/neat/core/{_graph → _instances}/transformers/__init__.py +0 -0
- /cognite/neat/core/{_graph → _instances}/transformers/_prune_graph.py +0 -0
- /cognite/neat/core/{_graph → _instances}/transformers/_rdfpath.py +0 -0
- {cognite_neat-0.121.0.dist-info → cognite_neat-0.121.2.dist-info}/WHEEL +0 -0
- {cognite_neat-0.121.0.dist-info → cognite_neat-0.121.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,58 +11,58 @@ import pandas as pd
|
|
|
11
11
|
from cognite.client import data_modeling as dm
|
|
12
12
|
from rdflib import URIRef
|
|
13
13
|
|
|
14
|
-
from cognite.neat.core.
|
|
15
|
-
from cognite.neat.core.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from cognite.neat.core.
|
|
20
|
-
|
|
14
|
+
from cognite.neat.core._data_model.models import ConceptualDataModel, PhysicalDataModel
|
|
15
|
+
from cognite.neat.core._data_model.models.conceptual import (
|
|
16
|
+
Concept,
|
|
17
|
+
ConceptualProperty,
|
|
18
|
+
)
|
|
19
|
+
from cognite.neat.core._data_model.models.entities import (
|
|
20
|
+
ConceptEntity,
|
|
21
21
|
MultiValueTypeInfo,
|
|
22
22
|
ViewEntity,
|
|
23
23
|
)
|
|
24
|
-
from cognite.neat.core.
|
|
24
|
+
from cognite.neat.core._data_model.models.entities._single_value import (
|
|
25
25
|
UnknownEntity,
|
|
26
26
|
)
|
|
27
|
-
from cognite.neat.core.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
from cognite.neat.core._data_model.models.physical import PhysicalProperty
|
|
28
|
+
from cognite.neat.core._data_model.models.physical._verified import PhysicalView
|
|
29
|
+
from cognite.neat.core._issues.errors import NeatValueError
|
|
30
|
+
from cognite.neat.core._issues.warnings import NeatValueWarning
|
|
31
31
|
|
|
32
32
|
T_Hashable = TypeVar("T_Hashable", bound=Hashable)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
@dataclass(frozen=True)
|
|
36
36
|
class Linkage:
|
|
37
|
-
|
|
37
|
+
source_concept: ConceptEntity
|
|
38
38
|
connecting_property: str
|
|
39
|
-
|
|
39
|
+
target_concept: ConceptEntity
|
|
40
40
|
max_occurrence: int | float | None
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
class LinkageSet(set, Set[Linkage]):
|
|
44
44
|
@property
|
|
45
|
-
def
|
|
46
|
-
return {link.
|
|
45
|
+
def source_concept(self) -> set[ConceptEntity]:
|
|
46
|
+
return {link.source_concept for link in self}
|
|
47
47
|
|
|
48
48
|
@property
|
|
49
|
-
def
|
|
50
|
-
return {link.
|
|
49
|
+
def target_concept(self) -> set[ConceptEntity]:
|
|
50
|
+
return {link.target_concept for link in self}
|
|
51
51
|
|
|
52
|
-
def
|
|
53
|
-
|
|
52
|
+
def get_target_concepts_by_source(self) -> dict[ConceptEntity, set[ConceptEntity]]:
|
|
53
|
+
target_concepts_by_source: dict[ConceptEntity, set[ConceptEntity]] = defaultdict(set)
|
|
54
54
|
for link in self:
|
|
55
|
-
|
|
56
|
-
return
|
|
55
|
+
target_concepts_by_source[link.source_concept].add(link.target_concept)
|
|
56
|
+
return target_concepts_by_source
|
|
57
57
|
|
|
58
58
|
def to_pandas(self) -> pd.DataFrame:
|
|
59
59
|
# Todo: Remove this method
|
|
60
60
|
return pd.DataFrame(
|
|
61
61
|
[
|
|
62
62
|
{
|
|
63
|
-
"
|
|
63
|
+
"source_concept": link.source_concept,
|
|
64
64
|
"connecting_property": link.connecting_property,
|
|
65
|
-
"
|
|
65
|
+
"target_concept": link.target_concept,
|
|
66
66
|
"max_occurrence": link.max_occurrence,
|
|
67
67
|
}
|
|
68
68
|
for link in self
|
|
@@ -110,105 +110,112 @@ class ViewQueryDict(dict, MutableMapping[dm.ViewId, ViewQuery]):
|
|
|
110
110
|
return super().popitem()
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
class
|
|
114
|
-
def __init__(
|
|
115
|
-
self
|
|
116
|
-
|
|
113
|
+
class DataModelAnalysis:
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
conceptual: ConceptualDataModel | None = None,
|
|
117
|
+
physical: PhysicalDataModel | None = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
self._conceptual = conceptual
|
|
120
|
+
self._physical = physical
|
|
117
121
|
|
|
118
122
|
@property
|
|
119
|
-
def
|
|
120
|
-
if self.
|
|
121
|
-
raise NeatValueError("
|
|
122
|
-
return self.
|
|
123
|
+
def conceptual(self) -> ConceptualDataModel:
|
|
124
|
+
if self._conceptual is None:
|
|
125
|
+
raise NeatValueError("Conceptual Data Model is required for this analysis")
|
|
126
|
+
return self._conceptual
|
|
123
127
|
|
|
124
128
|
@property
|
|
125
|
-
def
|
|
126
|
-
if self.
|
|
127
|
-
raise NeatValueError("
|
|
128
|
-
return self.
|
|
129
|
+
def physical(self) -> PhysicalDataModel:
|
|
130
|
+
if self._physical is None:
|
|
131
|
+
raise NeatValueError("Physical Data Model is required for this analysis")
|
|
132
|
+
return self._physical
|
|
129
133
|
|
|
130
|
-
def
|
|
134
|
+
def parents_by_concept(
|
|
131
135
|
self, include_ancestors: bool = False, include_different_space: bool = False
|
|
132
|
-
) -> dict[
|
|
133
|
-
"""Get a dictionary of
|
|
136
|
+
) -> dict[ConceptEntity, set[ConceptEntity]]:
|
|
137
|
+
"""Get a dictionary of concepts and their parents.
|
|
134
138
|
|
|
135
139
|
Args:
|
|
136
140
|
include_ancestors (bool, optional): Include ancestors of the parents. Defaults to False.
|
|
137
141
|
include_different_space (bool, optional): Include parents from different spaces. Defaults to False.
|
|
138
142
|
|
|
139
143
|
Returns:
|
|
140
|
-
dict[
|
|
144
|
+
dict[ConceptEntity, set[ConceptEntity]]: Values parents with concept as key.
|
|
141
145
|
"""
|
|
142
|
-
|
|
143
|
-
for
|
|
144
|
-
|
|
145
|
-
for parent in
|
|
146
|
-
if include_different_space or parent.prefix ==
|
|
147
|
-
|
|
146
|
+
parents_by_concept: dict[ConceptEntity, set[ConceptEntity]] = {}
|
|
147
|
+
for concept in self.conceptual.concepts:
|
|
148
|
+
parents_by_concept[concept.concept] = set()
|
|
149
|
+
for parent in concept.implements or []:
|
|
150
|
+
if include_different_space or parent.prefix == concept.concept.prefix:
|
|
151
|
+
parents_by_concept[concept.concept].add(parent)
|
|
148
152
|
else:
|
|
149
153
|
warnings.warn(
|
|
150
154
|
NeatValueWarning(
|
|
151
|
-
f"Parent
|
|
155
|
+
f"Parent concept {parent} of concept {concept} is not in the same namespace, skipping!"
|
|
152
156
|
),
|
|
153
157
|
stacklevel=2,
|
|
154
158
|
)
|
|
155
159
|
if include_ancestors:
|
|
156
|
-
self._include_ancestors(
|
|
160
|
+
self._include_ancestors(parents_by_concept)
|
|
157
161
|
|
|
158
|
-
return
|
|
162
|
+
return parents_by_concept
|
|
159
163
|
|
|
160
164
|
@staticmethod
|
|
161
|
-
def _include_ancestors(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
def _include_ancestors(
|
|
166
|
+
parents_by_concept: dict[T_Hashable, set[T_Hashable]],
|
|
167
|
+
) -> None:
|
|
168
|
+
# Topological sort to ensure that concepts include all ancestors
|
|
169
|
+
for concept_entity in list(TopologicalSorter(parents_by_concept).static_order()):
|
|
170
|
+
if concept_entity not in parents_by_concept:
|
|
165
171
|
continue
|
|
166
|
-
|
|
172
|
+
parents_by_concept[concept_entity] |= {
|
|
167
173
|
grand_parent
|
|
168
|
-
for parent in
|
|
169
|
-
for grand_parent in
|
|
174
|
+
for parent in parents_by_concept[concept_entity]
|
|
175
|
+
for grand_parent in parents_by_concept.get(parent, set())
|
|
170
176
|
}
|
|
171
177
|
|
|
172
|
-
def
|
|
178
|
+
def properties_by_concepts(
|
|
173
179
|
self, include_ancestors: bool = False, include_different_space: bool = False
|
|
174
|
-
) -> dict[
|
|
175
|
-
"""Get a dictionary of
|
|
180
|
+
) -> dict[ConceptEntity, list[ConceptualProperty]]:
|
|
181
|
+
"""Get a dictionary of concepts and their properties.
|
|
176
182
|
|
|
177
183
|
Args:
|
|
178
|
-
include_ancestors: Whether to include properties from parent
|
|
179
|
-
include_different_space: Whether to include properties from parent
|
|
184
|
+
include_ancestors: Whether to include properties from parent concepts.
|
|
185
|
+
include_different_space: Whether to include properties from parent concepts in different spaces.
|
|
180
186
|
|
|
181
187
|
Returns:
|
|
182
|
-
dict[
|
|
188
|
+
dict[ConceptEntity, list[ConceptualProperty]]: Values properties with concept as key.
|
|
183
189
|
|
|
184
190
|
"""
|
|
185
|
-
|
|
186
|
-
for prop in self.
|
|
187
|
-
|
|
191
|
+
properties_by_concepts: dict[ConceptEntity, list[ConceptualProperty]] = defaultdict(list)
|
|
192
|
+
for prop in self.conceptual.properties:
|
|
193
|
+
properties_by_concepts[prop.concept].append(prop)
|
|
188
194
|
|
|
189
195
|
if include_ancestors:
|
|
190
|
-
|
|
191
|
-
include_ancestors=include_ancestors,
|
|
196
|
+
parents_by_concepts = self.parents_by_concept(
|
|
197
|
+
include_ancestors=include_ancestors,
|
|
198
|
+
include_different_space=include_different_space,
|
|
192
199
|
)
|
|
193
|
-
for
|
|
194
|
-
|
|
200
|
+
for concept, parents in parents_by_concepts.items():
|
|
201
|
+
concept_properties = {prop.property_ for prop in properties_by_concepts[concept]}
|
|
195
202
|
for parent in parents:
|
|
196
|
-
for parent_prop in
|
|
197
|
-
if parent_prop.property_ not in
|
|
198
|
-
child_prop = parent_prop.model_copy(update={"
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
for parent_prop in properties_by_concepts[parent]:
|
|
204
|
+
if parent_prop.property_ not in concept_properties:
|
|
205
|
+
child_prop = parent_prop.model_copy(update={"concept": concept})
|
|
206
|
+
properties_by_concepts[concept].append(child_prop)
|
|
207
|
+
concept_properties.add(child_prop.property_)
|
|
201
208
|
|
|
202
|
-
return
|
|
209
|
+
return properties_by_concepts
|
|
203
210
|
|
|
204
211
|
def implements_by_view(
|
|
205
212
|
self, include_ancestors: bool = False, include_different_space: bool = False
|
|
206
213
|
) -> dict[ViewEntity, set[ViewEntity]]:
|
|
207
214
|
"""Get a dictionary of views and their implemented views."""
|
|
208
|
-
# This is a duplicate fo the
|
|
215
|
+
# This is a duplicate fo the parent_by_concept method, but for views
|
|
209
216
|
# The choice to duplicate the code is to avoid generics which will make the code less readable
|
|
210
217
|
implements_by_view: dict[ViewEntity, set[ViewEntity]] = {}
|
|
211
|
-
for view in self.
|
|
218
|
+
for view in self.physical.views:
|
|
212
219
|
implements_by_view[view.view] = set()
|
|
213
220
|
for implements in view.implements or []:
|
|
214
221
|
if include_different_space or implements.space == view.view.space:
|
|
@@ -226,12 +233,12 @@ class RulesAnalysis:
|
|
|
226
233
|
|
|
227
234
|
def properties_by_view(
|
|
228
235
|
self, include_ancestors: bool = False, include_different_space: bool = False
|
|
229
|
-
) -> dict[ViewEntity, list[
|
|
236
|
+
) -> dict[ViewEntity, list[PhysicalProperty]]:
|
|
230
237
|
"""Get a dictionary of views and their properties."""
|
|
231
|
-
# This is a duplicate fo the
|
|
238
|
+
# This is a duplicate fo the properties_by_concept method, but for views
|
|
232
239
|
# The choice to duplicate the code is to avoid generics which will make the code less readable.
|
|
233
|
-
properties_by_views: dict[ViewEntity, list[
|
|
234
|
-
for prop in self.
|
|
240
|
+
properties_by_views: dict[ViewEntity, list[PhysicalProperty]] = defaultdict(list)
|
|
241
|
+
for prop in self.physical.properties:
|
|
235
242
|
properties_by_views[prop.view].append(prop)
|
|
236
243
|
|
|
237
244
|
if include_ancestors:
|
|
@@ -250,11 +257,11 @@ class RulesAnalysis:
|
|
|
250
257
|
return properties_by_views
|
|
251
258
|
|
|
252
259
|
@property
|
|
253
|
-
def
|
|
260
|
+
def conceptual_uri_by_view(self) -> dict[ViewEntity, URIRef]:
|
|
254
261
|
"""Get the logical URI by view."""
|
|
255
|
-
return {view.view: view.
|
|
262
|
+
return {view.view: view.conceptual for view in self.physical.views if view.conceptual}
|
|
256
263
|
|
|
257
|
-
def
|
|
264
|
+
def conceptual_uri_by_property_by_view(
|
|
258
265
|
self,
|
|
259
266
|
include_ancestors: bool = False,
|
|
260
267
|
include_different_space: bool = False,
|
|
@@ -263,68 +270,68 @@ class RulesAnalysis:
|
|
|
263
270
|
properties_by_view = self.properties_by_view(include_ancestors, include_different_space)
|
|
264
271
|
|
|
265
272
|
return {
|
|
266
|
-
view: {prop.view_property: prop.
|
|
273
|
+
view: {prop.view_property: prop.conceptual for prop in properties if prop.conceptual}
|
|
267
274
|
for view, properties in properties_by_view.items()
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
@property
|
|
271
|
-
def
|
|
272
|
-
"""Get a dictionary of
|
|
273
|
-
|
|
278
|
+
def _concept_by_neat_id(self) -> dict[URIRef, Concept]:
|
|
279
|
+
"""Get a dictionary of concept neat IDs to
|
|
280
|
+
concept entities."""
|
|
274
281
|
|
|
275
|
-
return {cls.neatId: cls for cls in self.
|
|
282
|
+
return {cls.neatId: cls for cls in self.conceptual.concepts if cls.neatId}
|
|
276
283
|
|
|
277
|
-
def
|
|
278
|
-
"""Get a dictionary of
|
|
284
|
+
def concept_by_suffix(self) -> dict[str, Concept]:
|
|
285
|
+
"""Get a dictionary of concept suffixes to concept entities."""
|
|
279
286
|
# TODO: Remove this method
|
|
280
|
-
|
|
281
|
-
for definition in self.
|
|
282
|
-
entity = definition.
|
|
283
|
-
if entity.suffix in
|
|
287
|
+
concept_dict: dict[str, Concept] = {}
|
|
288
|
+
for definition in self.conceptual.concepts:
|
|
289
|
+
entity = definition.concept
|
|
290
|
+
if entity.suffix in concept_dict:
|
|
284
291
|
warnings.warn(
|
|
285
292
|
NeatValueWarning(
|
|
286
|
-
f"
|
|
293
|
+
f"Concept {entity} has been defined more than once! Only the first definition "
|
|
287
294
|
"will be considered, skipping the rest.."
|
|
288
295
|
),
|
|
289
296
|
stacklevel=2,
|
|
290
297
|
)
|
|
291
298
|
continue
|
|
292
|
-
|
|
293
|
-
return
|
|
299
|
+
concept_dict[entity.suffix] = definition
|
|
300
|
+
return concept_dict
|
|
294
301
|
|
|
295
302
|
@property
|
|
296
|
-
def
|
|
297
|
-
"""Get a dictionary of
|
|
298
|
-
|
|
299
|
-
return {cls.
|
|
303
|
+
def concept_by_concept_entity(self) -> dict[ConceptEntity, Concept]:
|
|
304
|
+
"""Get a dictionary of concept entities to concept entities."""
|
|
305
|
+
data_model = self.conceptual
|
|
306
|
+
return {cls.concept: cls for cls in data_model.concepts}
|
|
300
307
|
|
|
301
308
|
@property
|
|
302
|
-
def view_by_view_entity(self) -> dict[ViewEntity,
|
|
303
|
-
"""Get a dictionary of
|
|
304
|
-
|
|
305
|
-
return {view.view: view for view in
|
|
309
|
+
def view_by_view_entity(self) -> dict[ViewEntity, PhysicalView]:
|
|
310
|
+
"""Get a dictionary of views to view entities."""
|
|
311
|
+
data_model = self.physical
|
|
312
|
+
return {view.view: view for view in data_model.views}
|
|
306
313
|
|
|
307
|
-
def property_by_id(self) -> dict[str, list[
|
|
314
|
+
def property_by_id(self) -> dict[str, list[ConceptualProperty]]:
|
|
308
315
|
"""Get a dictionary of property IDs to property entities."""
|
|
309
|
-
property_dict: dict[str, list[
|
|
310
|
-
for prop in self.
|
|
316
|
+
property_dict: dict[str, list[ConceptualProperty]] = defaultdict(list)
|
|
317
|
+
for prop in self.conceptual.properties:
|
|
311
318
|
property_dict[prop.property_].append(prop)
|
|
312
319
|
return property_dict
|
|
313
320
|
|
|
314
|
-
def
|
|
321
|
+
def properties_by_id_by_concept(
|
|
315
322
|
self,
|
|
316
323
|
has_instance_source: bool = False,
|
|
317
324
|
include_ancestors: bool = False,
|
|
318
|
-
) -> dict[
|
|
319
|
-
"""Get a dictionary of
|
|
320
|
-
|
|
321
|
-
for
|
|
322
|
-
processed_properties: dict[str,
|
|
325
|
+
) -> dict[ConceptEntity, dict[str, ConceptualProperty]]:
|
|
326
|
+
"""Get a dictionary of concept entities to dictionaries of property IDs to property entities."""
|
|
327
|
+
concept_property_pairs: dict[ConceptEntity, dict[str, ConceptualProperty]] = {}
|
|
328
|
+
for concept, properties in self.properties_by_concepts(include_ancestors).items():
|
|
329
|
+
processed_properties: dict[str, ConceptualProperty] = {}
|
|
323
330
|
for prop in properties:
|
|
324
331
|
if prop.property_ in processed_properties:
|
|
325
332
|
warnings.warn(
|
|
326
333
|
NeatValueWarning(
|
|
327
|
-
f"Property {processed_properties} for {
|
|
334
|
+
f"Property {processed_properties} for {concept} has been defined more than once!"
|
|
328
335
|
" Only the first definition will be considered, skipping the rest.."
|
|
329
336
|
),
|
|
330
337
|
stacklevel=2,
|
|
@@ -333,34 +340,34 @@ class RulesAnalysis:
|
|
|
333
340
|
if has_instance_source and prop.instance_source is None:
|
|
334
341
|
continue
|
|
335
342
|
processed_properties[prop.property_] = prop
|
|
336
|
-
|
|
343
|
+
concept_property_pairs[concept] = processed_properties
|
|
337
344
|
|
|
338
|
-
return
|
|
345
|
+
return concept_property_pairs
|
|
339
346
|
|
|
340
347
|
def defined_views(self, include_ancestors: bool = False) -> set[ViewEntity]:
|
|
341
348
|
properties_by_view = self.properties_by_view(include_ancestors)
|
|
342
349
|
return {prop.view for prop in itertools.chain.from_iterable(properties_by_view.values())}
|
|
343
350
|
|
|
344
|
-
def
|
|
351
|
+
def defined_concepts(
|
|
345
352
|
self,
|
|
346
353
|
include_ancestors: bool = False,
|
|
347
|
-
) -> set[
|
|
348
|
-
"""Returns
|
|
354
|
+
) -> set[ConceptEntity]:
|
|
355
|
+
"""Returns concepts that have properties defined for them in the data model.
|
|
349
356
|
|
|
350
357
|
Args:
|
|
351
358
|
include_ancestors: Whether to consider inheritance or not. Defaults False
|
|
352
359
|
|
|
353
360
|
Returns:
|
|
354
|
-
Set of
|
|
361
|
+
Set of concepts that have been defined in the data model
|
|
355
362
|
"""
|
|
356
|
-
|
|
357
|
-
return {prop.
|
|
363
|
+
properties_by_concept = self.properties_by_concepts(include_ancestors)
|
|
364
|
+
return {prop.concept for prop in itertools.chain.from_iterable(properties_by_concept.values())}
|
|
358
365
|
|
|
359
|
-
def
|
|
366
|
+
def concept_linkage(
|
|
360
367
|
self,
|
|
361
368
|
include_ancestors: bool = False,
|
|
362
369
|
) -> LinkageSet:
|
|
363
|
-
"""Returns a set of
|
|
370
|
+
"""Returns a set of concept linkages in the data model.
|
|
364
371
|
|
|
365
372
|
Args:
|
|
366
373
|
include_ancestors: Whether to consider inheritance or not. Defaults False
|
|
@@ -368,105 +375,105 @@ class RulesAnalysis:
|
|
|
368
375
|
Returns:
|
|
369
376
|
|
|
370
377
|
"""
|
|
371
|
-
|
|
378
|
+
concept_linkage = LinkageSet()
|
|
372
379
|
|
|
373
|
-
|
|
380
|
+
properties_by_concept = self.properties_by_concepts(include_ancestors)
|
|
374
381
|
|
|
375
|
-
prop:
|
|
376
|
-
for prop in itertools.chain.from_iterable(
|
|
377
|
-
if not isinstance(prop.value_type,
|
|
382
|
+
prop: ConceptualProperty
|
|
383
|
+
for prop in itertools.chain.from_iterable(properties_by_concept.values()):
|
|
384
|
+
if not isinstance(prop.value_type, ConceptEntity):
|
|
378
385
|
continue
|
|
379
|
-
|
|
386
|
+
concept_linkage.add(
|
|
380
387
|
Linkage(
|
|
381
|
-
|
|
388
|
+
source_concept=prop.concept,
|
|
382
389
|
connecting_property=prop.property_,
|
|
383
|
-
|
|
390
|
+
target_concept=prop.value_type,
|
|
384
391
|
max_occurrence=prop.max_count,
|
|
385
392
|
)
|
|
386
393
|
)
|
|
387
394
|
|
|
388
|
-
return
|
|
395
|
+
return concept_linkage
|
|
389
396
|
|
|
390
|
-
def
|
|
397
|
+
def symmetrically_connected_concepts(
|
|
391
398
|
self,
|
|
392
399
|
include_ancestors: bool = False,
|
|
393
|
-
) -> set[tuple[
|
|
394
|
-
"""Returns a set of pairs of symmetrically linked
|
|
400
|
+
) -> set[tuple[ConceptEntity, ConceptEntity]]:
|
|
401
|
+
"""Returns a set of pairs of symmetrically linked concepts.
|
|
395
402
|
|
|
396
403
|
Args:
|
|
397
404
|
include_ancestors: Whether to consider inheritance or not. Defaults False
|
|
398
405
|
|
|
399
406
|
Returns:
|
|
400
|
-
Set of pairs of symmetrically linked
|
|
407
|
+
Set of pairs of symmetrically linked concepts
|
|
401
408
|
|
|
402
|
-
!!! note "Symmetrically Connected
|
|
403
|
-
Symmetrically connected
|
|
404
|
-
in both directions. For example, if
|
|
405
|
-
is connected to
|
|
409
|
+
!!! note "Symmetrically Connected Concepts"
|
|
410
|
+
Symmetrically connected concepts are concepts that are connected to each other
|
|
411
|
+
in both directions. For example, if concept A is connected to concept B, and concept B
|
|
412
|
+
is connected to concept A, then concepts A and B are symmetrically connected.
|
|
406
413
|
"""
|
|
407
|
-
sym_pairs: set[tuple[
|
|
408
|
-
|
|
409
|
-
if not
|
|
414
|
+
sym_pairs: set[tuple[ConceptEntity, ConceptEntity]] = set()
|
|
415
|
+
concept_linkage = self.concept_linkage(include_ancestors)
|
|
416
|
+
if not concept_linkage:
|
|
410
417
|
return sym_pairs
|
|
411
418
|
|
|
412
|
-
targets_by_source =
|
|
413
|
-
for link in
|
|
414
|
-
source = link.
|
|
415
|
-
target = link.
|
|
419
|
+
targets_by_source = concept_linkage.get_target_concepts_by_source()
|
|
420
|
+
for link in concept_linkage:
|
|
421
|
+
source = link.source_concept
|
|
422
|
+
target = link.target_concept
|
|
416
423
|
|
|
417
424
|
if source in targets_by_source[source] and (source, target) not in sym_pairs:
|
|
418
425
|
sym_pairs.add((source, target))
|
|
419
426
|
return sym_pairs
|
|
420
427
|
|
|
421
428
|
@overload
|
|
422
|
-
def _properties_by_neat_id(self, format: Literal["info"] = "info") -> dict[URIRef,
|
|
429
|
+
def _properties_by_neat_id(self, format: Literal["info"] = "info") -> dict[URIRef, ConceptualProperty]: ...
|
|
423
430
|
|
|
424
431
|
@overload
|
|
425
|
-
def _properties_by_neat_id(self, format: Literal["dms"] = "dms") -> dict[URIRef,
|
|
432
|
+
def _properties_by_neat_id(self, format: Literal["dms"] = "dms") -> dict[URIRef, PhysicalProperty]: ...
|
|
426
433
|
|
|
427
434
|
def _properties_by_neat_id(
|
|
428
435
|
self, format: Literal["info", "dms"] = "info"
|
|
429
|
-
) -> dict[URIRef,
|
|
436
|
+
) -> dict[URIRef, ConceptualProperty] | dict[URIRef, PhysicalProperty]:
|
|
430
437
|
if format == "info":
|
|
431
|
-
return {prop.neatId: prop for prop in self.
|
|
438
|
+
return {prop.neatId: prop for prop in self.conceptual.properties if prop.neatId}
|
|
432
439
|
elif format == "dms":
|
|
433
|
-
return {prop.neatId: prop for prop in self.
|
|
440
|
+
return {prop.neatId: prop for prop in self.physical.properties if prop.neatId}
|
|
434
441
|
else:
|
|
435
442
|
raise NeatValueError(f"Invalid format: {format}")
|
|
436
443
|
|
|
437
444
|
@property
|
|
438
|
-
def
|
|
439
|
-
return {
|
|
445
|
+
def concepts_by_neat_id(self) -> dict[URIRef, Concept]:
|
|
446
|
+
return {concept.neatId: concept for concept in self.conceptual.concepts if concept.neatId}
|
|
440
447
|
|
|
441
448
|
@property
|
|
442
|
-
def multi_value_properties(self) -> list[
|
|
443
|
-
return [prop_ for prop_ in self.
|
|
449
|
+
def multi_value_properties(self) -> list[ConceptualProperty]:
|
|
450
|
+
return [prop_ for prop_ in self.conceptual.properties if isinstance(prop_.value_type, MultiValueTypeInfo)]
|
|
444
451
|
|
|
445
452
|
@property
|
|
446
453
|
def view_query_by_id(
|
|
447
454
|
self,
|
|
448
455
|
) -> "ViewQueryDict":
|
|
449
456
|
# Trigger error if any of these are missing
|
|
450
|
-
_ = self.
|
|
451
|
-
_ = self.
|
|
457
|
+
_ = self.conceptual
|
|
458
|
+
_ = self.physical
|
|
452
459
|
|
|
453
460
|
# caching results for faster access
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
461
|
+
concepts_by_neat_id = self._concept_by_neat_id
|
|
462
|
+
properties_by_concept = self.properties_by_concepts(include_ancestors=True)
|
|
463
|
+
conceptual_uri_by_view = self.conceptual_uri_by_view
|
|
464
|
+
conceptual_uri_by_property_by_view = self.conceptual_uri_by_property_by_view(include_ancestors=True)
|
|
465
|
+
conceptual_properties_by_neat_id = self._properties_by_neat_id()
|
|
459
466
|
|
|
460
467
|
query_configs = ViewQueryDict()
|
|
461
|
-
for view in self.
|
|
468
|
+
for view in self.physical.views:
|
|
462
469
|
# this entire block of sequential if statements checks:
|
|
463
|
-
# 1. connection of
|
|
464
|
-
# 2. correct paring of
|
|
465
|
-
# 3. connection of
|
|
470
|
+
# 1. connection of physical and conceptual data model
|
|
471
|
+
# 2. correct paring of conceptual and physical data model
|
|
472
|
+
# 3. connection of conceptual data model to instances
|
|
466
473
|
if (
|
|
467
|
-
(neat_id :=
|
|
468
|
-
and (
|
|
469
|
-
and (uri :=
|
|
474
|
+
(neat_id := conceptual_uri_by_view.get(view.view))
|
|
475
|
+
and (concept := concepts_by_neat_id.get(neat_id))
|
|
476
|
+
and (uri := concept.instance_source)
|
|
470
477
|
):
|
|
471
478
|
view_query = ViewQuery(
|
|
472
479
|
view_id=view.view.as_id(),
|
|
@@ -475,14 +482,14 @@ class RulesAnalysis:
|
|
|
475
482
|
# this is to encounter for special cases of e.g. space, startNode and endNode
|
|
476
483
|
property_renaming_config=(
|
|
477
484
|
{uri: prop_.property_ for prop_ in info_properties for uri in prop_.instance_source or []}
|
|
478
|
-
if (info_properties :=
|
|
485
|
+
if (info_properties := properties_by_concept.get(concept.concept))
|
|
479
486
|
else {}
|
|
480
487
|
),
|
|
481
488
|
)
|
|
482
489
|
|
|
483
|
-
if
|
|
484
|
-
for target_name, neat_id in
|
|
485
|
-
if (property_ :=
|
|
490
|
+
if conceptual_uri_by_property := conceptual_uri_by_property_by_view.get(view.view):
|
|
491
|
+
for target_name, neat_id in conceptual_uri_by_property.items():
|
|
492
|
+
if (property_ := conceptual_properties_by_neat_id.get(neat_id)) and (
|
|
486
493
|
uris := property_.instance_source
|
|
487
494
|
):
|
|
488
495
|
for uri in uris:
|
|
@@ -492,14 +499,14 @@ class RulesAnalysis:
|
|
|
492
499
|
|
|
493
500
|
return query_configs
|
|
494
501
|
|
|
495
|
-
def
|
|
496
|
-
"""Generate a MultiDiGraph from the
|
|
502
|
+
def _physical_di_graph(self, format: Literal["data-model", "implements"] = "data-model") -> nx.MultiDiGraph:
|
|
503
|
+
"""Generate a MultiDiGraph from the Physical Data Model."""
|
|
497
504
|
di_graph = nx.MultiDiGraph()
|
|
498
505
|
|
|
499
|
-
|
|
506
|
+
data_model = self.physical
|
|
500
507
|
|
|
501
508
|
# Add nodes and edges from Views sheet
|
|
502
|
-
for view in
|
|
509
|
+
for view in data_model.views:
|
|
503
510
|
di_graph.add_node(view.view.suffix, label=view.view.suffix)
|
|
504
511
|
|
|
505
512
|
if format == "implements" and view.implements:
|
|
@@ -514,7 +521,7 @@ class RulesAnalysis:
|
|
|
514
521
|
|
|
515
522
|
if format == "data-model":
|
|
516
523
|
# Add nodes and edges from Properties sheet
|
|
517
|
-
for prop_ in
|
|
524
|
+
for prop_ in data_model.properties:
|
|
518
525
|
if prop_.connection and isinstance(prop_.value_type, ViewEntity):
|
|
519
526
|
di_graph.add_node(prop_.view.suffix, label=prop_.view.suffix)
|
|
520
527
|
di_graph.add_node(prop_.value_type.suffix, label=prop_.value_type.suffix)
|
|
@@ -526,26 +533,26 @@ class RulesAnalysis:
|
|
|
526
533
|
|
|
527
534
|
return di_graph
|
|
528
535
|
|
|
529
|
-
def
|
|
530
|
-
"""Generate MultiDiGraph representing
|
|
536
|
+
def _conceptual_di_graph(self, format: Literal["data-model", "implements"] = "data-model") -> nx.MultiDiGraph:
|
|
537
|
+
"""Generate MultiDiGraph representing conceptual data model."""
|
|
531
538
|
|
|
532
|
-
|
|
539
|
+
data_model = self.conceptual
|
|
533
540
|
di_graph = nx.MultiDiGraph()
|
|
534
541
|
|
|
535
542
|
# Add nodes and edges from Views sheet
|
|
536
|
-
for
|
|
543
|
+
for concept in data_model.concepts:
|
|
537
544
|
# if possible use human readable label coming from the view name
|
|
538
545
|
|
|
539
546
|
di_graph.add_node(
|
|
540
|
-
|
|
541
|
-
label=
|
|
547
|
+
concept.concept.suffix,
|
|
548
|
+
label=concept.name or concept.concept.suffix,
|
|
542
549
|
)
|
|
543
550
|
|
|
544
|
-
if format == "implements" and
|
|
545
|
-
for parent in
|
|
551
|
+
if format == "implements" and concept.implements:
|
|
552
|
+
for parent in concept.implements:
|
|
546
553
|
di_graph.add_node(parent.suffix, label=parent.suffix)
|
|
547
554
|
di_graph.add_edge(
|
|
548
|
-
|
|
555
|
+
concept.concept.suffix,
|
|
549
556
|
parent.suffix,
|
|
550
557
|
label="implements",
|
|
551
558
|
dashes=True,
|
|
@@ -553,13 +560,13 @@ class RulesAnalysis:
|
|
|
553
560
|
|
|
554
561
|
if format == "data-model":
|
|
555
562
|
# Add nodes and edges from Properties sheet
|
|
556
|
-
for prop_ in
|
|
557
|
-
if isinstance(prop_.value_type,
|
|
558
|
-
di_graph.add_node(prop_.
|
|
563
|
+
for prop_ in data_model.properties:
|
|
564
|
+
if isinstance(prop_.value_type, ConceptEntity) and not isinstance(prop_.value_type, UnknownEntity):
|
|
565
|
+
di_graph.add_node(prop_.concept.suffix, label=prop_.concept.suffix)
|
|
559
566
|
di_graph.add_node(prop_.value_type.suffix, label=prop_.value_type.suffix)
|
|
560
567
|
|
|
561
568
|
di_graph.add_edge(
|
|
562
|
-
prop_.
|
|
569
|
+
prop_.concept.suffix,
|
|
563
570
|
prop_.value_type.suffix,
|
|
564
571
|
label=prop_.name or prop_.property_,
|
|
565
572
|
)
|