cognite-neat 0.90.2__py3-none-any.whl → 0.92.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cognite-neat might be problematic. Click here for more details.
- cognite/neat/_version.py +1 -1
- cognite/neat/graph/extractors/__init__.py +5 -0
- cognite/neat/graph/extractors/_classic_cdf/_assets.py +8 -8
- cognite/neat/graph/extractors/_classic_cdf/_base.py +26 -1
- cognite/neat/graph/extractors/_classic_cdf/_classic.py +208 -0
- cognite/neat/graph/extractors/_classic_cdf/_data_sets.py +110 -0
- cognite/neat/graph/extractors/_classic_cdf/_events.py +30 -5
- cognite/neat/graph/extractors/_classic_cdf/_files.py +33 -8
- cognite/neat/graph/extractors/_classic_cdf/_labels.py +14 -6
- cognite/neat/graph/extractors/_classic_cdf/_relationships.py +38 -7
- cognite/neat/graph/extractors/_classic_cdf/_sequences.py +30 -5
- cognite/neat/graph/extractors/_classic_cdf/_timeseries.py +30 -5
- cognite/neat/graph/extractors/_dexpi.py +4 -4
- cognite/neat/graph/extractors/_iodd.py +160 -0
- cognite/neat/issues/_base.py +6 -2
- cognite/neat/rules/exporters/_rules2excel.py +3 -3
- cognite/neat/rules/exporters/_rules2yaml.py +5 -1
- cognite/neat/rules/models/__init__.py +2 -2
- cognite/neat/rules/models/_base_input.py +2 -2
- cognite/neat/rules/models/_base_rules.py +142 -142
- cognite/neat/rules/models/asset/_rules.py +1 -34
- cognite/neat/rules/models/dms/_rules.py +127 -46
- cognite/neat/rules/models/dms/_validation.py +2 -2
- cognite/neat/rules/models/domain.py +16 -19
- cognite/neat/rules/models/entities/_single_value.py +25 -11
- cognite/neat/rules/models/entities/_types.py +0 -10
- cognite/neat/rules/models/information/_rules.py +68 -43
- cognite/neat/rules/models/information/_validation.py +5 -5
- cognite/neat/rules/transformers/_converters.py +6 -8
- cognite/neat/rules/transformers/_pipelines.py +8 -4
- cognite/neat/store/_base.py +1 -1
- cognite/neat/utils/collection_.py +4 -3
- cognite/neat/utils/xml_.py +27 -12
- {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/RECORD +38 -38
- cognite/neat/rules/models/asset/_serializer.py +0 -73
- cognite/neat/rules/models/dms/_serializer.py +0 -157
- cognite/neat/rules/models/information/_serializer.py +0 -73
- {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.90.2.dist-info → cognite_neat-0.92.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from collections
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from collections.abc import Callable, Iterable, Set
|
|
2
3
|
from datetime import datetime, timezone
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from urllib.parse import quote
|
|
5
5
|
|
|
6
6
|
from cognite.client import CogniteClient
|
|
7
7
|
from cognite.client.data_classes import Relationship, RelationshipList
|
|
@@ -10,10 +10,11 @@ from rdflib import RDF, Literal, Namespace
|
|
|
10
10
|
from cognite.neat.graph.models import Triple
|
|
11
11
|
from cognite.neat.utils.auxiliary import create_sha256_hash
|
|
12
12
|
|
|
13
|
-
from ._base import DEFAULT_SKIP_METADATA_VALUES,
|
|
13
|
+
from ._base import DEFAULT_SKIP_METADATA_VALUES, ClassicCDFBaseExtractor, InstanceIdPrefix
|
|
14
|
+
from ._labels import LabelsExtractor
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class RelationshipsExtractor(
|
|
17
|
+
class RelationshipsExtractor(ClassicCDFBaseExtractor[Relationship]):
|
|
17
18
|
"""Extract data from Cognite Data Fusions Relationships into Neat.
|
|
18
19
|
|
|
19
20
|
Args:
|
|
@@ -34,6 +35,31 @@ class RelationshipsExtractor(ClassicCDFExtractor[Relationship]):
|
|
|
34
35
|
|
|
35
36
|
_default_rdf_type = "Relationship"
|
|
36
37
|
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
items: Iterable[Relationship],
|
|
41
|
+
namespace: Namespace | None = None,
|
|
42
|
+
to_type: Callable[[Relationship], str | None] | None = None,
|
|
43
|
+
total: int | None = None,
|
|
44
|
+
limit: int | None = None,
|
|
45
|
+
unpack_metadata: bool = True,
|
|
46
|
+
skip_metadata_values: Set[str] | None = DEFAULT_SKIP_METADATA_VALUES,
|
|
47
|
+
):
|
|
48
|
+
super().__init__(
|
|
49
|
+
items,
|
|
50
|
+
namespace=namespace,
|
|
51
|
+
to_type=to_type,
|
|
52
|
+
total=total,
|
|
53
|
+
limit=limit,
|
|
54
|
+
unpack_metadata=unpack_metadata,
|
|
55
|
+
skip_metadata_values=skip_metadata_values,
|
|
56
|
+
)
|
|
57
|
+
# This is used by the ClassicExtractor to log the target nodes, such
|
|
58
|
+
# that it can extract them.
|
|
59
|
+
# It is private to avoid exposing it to the user.
|
|
60
|
+
self._log_target_nodes = False
|
|
61
|
+
self._target_external_ids_by_type: dict[InstanceIdPrefix, set[str]] = defaultdict(set)
|
|
62
|
+
|
|
37
63
|
@classmethod
|
|
38
64
|
def from_dataset(
|
|
39
65
|
cls,
|
|
@@ -79,8 +105,13 @@ class RelationshipsExtractor(ClassicCDFExtractor[Relationship]):
|
|
|
79
105
|
"""Converts an asset to triples."""
|
|
80
106
|
|
|
81
107
|
if relationship.external_id and relationship.source_external_id and relationship.target_external_id:
|
|
108
|
+
if self._log_target_nodes and relationship.target_type and relationship.target_external_id:
|
|
109
|
+
self._target_external_ids_by_type[InstanceIdPrefix.from_str(relationship.target_type)].add(
|
|
110
|
+
relationship.target_external_id
|
|
111
|
+
)
|
|
112
|
+
|
|
82
113
|
# relationships do not have an internal id, so we generate one
|
|
83
|
-
id_ = self.namespace[f"
|
|
114
|
+
id_ = self.namespace[f"{InstanceIdPrefix.relationship}{create_sha256_hash(relationship.external_id)}"]
|
|
84
115
|
|
|
85
116
|
type_ = self._get_rdf_type(relationship)
|
|
86
117
|
# Set rdf type
|
|
@@ -178,7 +209,7 @@ class RelationshipsExtractor(ClassicCDFExtractor[Relationship]):
|
|
|
178
209
|
(
|
|
179
210
|
id_,
|
|
180
211
|
self.namespace.label,
|
|
181
|
-
self.namespace[f"
|
|
212
|
+
self.namespace[f"{InstanceIdPrefix.label}{LabelsExtractor._label_id(label)}"],
|
|
182
213
|
)
|
|
183
214
|
)
|
|
184
215
|
|
|
@@ -188,7 +219,7 @@ class RelationshipsExtractor(ClassicCDFExtractor[Relationship]):
|
|
|
188
219
|
(
|
|
189
220
|
id_,
|
|
190
221
|
self.namespace.dataset,
|
|
191
|
-
self.namespace[f"
|
|
222
|
+
self.namespace[f"{InstanceIdPrefix.data_set}{relationship.data_set_id}"],
|
|
192
223
|
)
|
|
193
224
|
)
|
|
194
225
|
|
|
@@ -8,10 +8,10 @@ from rdflib import RDF, Literal, Namespace
|
|
|
8
8
|
|
|
9
9
|
from cognite.neat.graph.models import Triple
|
|
10
10
|
|
|
11
|
-
from ._base import DEFAULT_SKIP_METADATA_VALUES,
|
|
11
|
+
from ._base import DEFAULT_SKIP_METADATA_VALUES, ClassicCDFBaseExtractor, InstanceIdPrefix
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class SequencesExtractor(
|
|
14
|
+
class SequencesExtractor(ClassicCDFBaseExtractor[Sequence]):
|
|
15
15
|
"""Extract data from Cognite Data Fusions Sequences into Neat.
|
|
16
16
|
|
|
17
17
|
Args:
|
|
@@ -56,6 +56,31 @@ class SequencesExtractor(ClassicCDFExtractor[Sequence]):
|
|
|
56
56
|
skip_metadata_values=skip_metadata_values,
|
|
57
57
|
)
|
|
58
58
|
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_hierarchy(
|
|
61
|
+
cls,
|
|
62
|
+
client: CogniteClient,
|
|
63
|
+
root_asset_external_id: str,
|
|
64
|
+
namespace: Namespace | None = None,
|
|
65
|
+
to_type: Callable[[Sequence], str | None] | None = None,
|
|
66
|
+
limit: int | None = None,
|
|
67
|
+
unpack_metadata: bool = True,
|
|
68
|
+
skip_metadata_values: Set[str] | None = DEFAULT_SKIP_METADATA_VALUES,
|
|
69
|
+
):
|
|
70
|
+
total = client.sequences.aggregate_count(
|
|
71
|
+
filter=SequenceFilter(asset_subtree_ids=[{"externalId": root_asset_external_id}])
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return cls(
|
|
75
|
+
client.sequences(asset_subtree_external_ids=[root_asset_external_id]),
|
|
76
|
+
namespace,
|
|
77
|
+
to_type,
|
|
78
|
+
total,
|
|
79
|
+
limit,
|
|
80
|
+
unpack_metadata=unpack_metadata,
|
|
81
|
+
skip_metadata_values=skip_metadata_values,
|
|
82
|
+
)
|
|
83
|
+
|
|
59
84
|
@classmethod
|
|
60
85
|
def from_file(
|
|
61
86
|
cls,
|
|
@@ -78,7 +103,7 @@ class SequencesExtractor(ClassicCDFExtractor[Sequence]):
|
|
|
78
103
|
)
|
|
79
104
|
|
|
80
105
|
def _item2triples(self, sequence: Sequence) -> list[Triple]:
|
|
81
|
-
id_ = self.namespace[f"
|
|
106
|
+
id_ = self.namespace[f"{InstanceIdPrefix.sequence}{sequence.id}"]
|
|
82
107
|
|
|
83
108
|
type_ = self._get_rdf_type(sequence)
|
|
84
109
|
# Set rdf type
|
|
@@ -121,7 +146,7 @@ class SequencesExtractor(ClassicCDFExtractor[Sequence]):
|
|
|
121
146
|
(
|
|
122
147
|
id_,
|
|
123
148
|
self.namespace.data_set_id,
|
|
124
|
-
self.namespace[f"
|
|
149
|
+
self.namespace[f"{InstanceIdPrefix.data_set}{sequence.data_set_id}"],
|
|
125
150
|
)
|
|
126
151
|
)
|
|
127
152
|
|
|
@@ -130,7 +155,7 @@ class SequencesExtractor(ClassicCDFExtractor[Sequence]):
|
|
|
130
155
|
(
|
|
131
156
|
id_,
|
|
132
157
|
self.namespace.asset,
|
|
133
|
-
self.namespace[f"
|
|
158
|
+
self.namespace[f"{InstanceIdPrefix.asset}{sequence.asset_id}"],
|
|
134
159
|
)
|
|
135
160
|
)
|
|
136
161
|
|
|
@@ -9,10 +9,10 @@ from rdflib import RDF, Literal, Namespace, URIRef
|
|
|
9
9
|
|
|
10
10
|
from cognite.neat.graph.models import Triple
|
|
11
11
|
|
|
12
|
-
from ._base import DEFAULT_SKIP_METADATA_VALUES,
|
|
12
|
+
from ._base import DEFAULT_SKIP_METADATA_VALUES, ClassicCDFBaseExtractor, InstanceIdPrefix
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class TimeSeriesExtractor(
|
|
15
|
+
class TimeSeriesExtractor(ClassicCDFBaseExtractor[TimeSeries]):
|
|
16
16
|
"""Extract data from Cognite Data Fusions TimeSeries into Neat.
|
|
17
17
|
|
|
18
18
|
Args:
|
|
@@ -58,6 +58,31 @@ class TimeSeriesExtractor(ClassicCDFExtractor[TimeSeries]):
|
|
|
58
58
|
skip_metadata_values=skip_metadata_values,
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_hierarchy(
|
|
63
|
+
cls,
|
|
64
|
+
client: CogniteClient,
|
|
65
|
+
root_asset_external_id: str,
|
|
66
|
+
namespace: Namespace | None = None,
|
|
67
|
+
to_type: Callable[[TimeSeries], str | None] | None = None,
|
|
68
|
+
limit: int | None = None,
|
|
69
|
+
unpack_metadata: bool = True,
|
|
70
|
+
skip_metadata_values: Set[str] | None = DEFAULT_SKIP_METADATA_VALUES,
|
|
71
|
+
):
|
|
72
|
+
total = client.time_series.aggregate_count(
|
|
73
|
+
filter=TimeSeriesFilter(asset_subtree_ids=[{"externalId": root_asset_external_id}])
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return cls(
|
|
77
|
+
client.time_series(asset_external_ids=[root_asset_external_id]),
|
|
78
|
+
namespace,
|
|
79
|
+
to_type,
|
|
80
|
+
total,
|
|
81
|
+
limit,
|
|
82
|
+
unpack_metadata=unpack_metadata,
|
|
83
|
+
skip_metadata_values=skip_metadata_values,
|
|
84
|
+
)
|
|
85
|
+
|
|
61
86
|
@classmethod
|
|
62
87
|
def from_file(
|
|
63
88
|
cls,
|
|
@@ -80,7 +105,7 @@ class TimeSeriesExtractor(ClassicCDFExtractor[TimeSeries]):
|
|
|
80
105
|
)
|
|
81
106
|
|
|
82
107
|
def _item2triples(self, timeseries: TimeSeries) -> list[Triple]:
|
|
83
|
-
id_ = self.namespace[f"
|
|
108
|
+
id_ = self.namespace[f"{InstanceIdPrefix.time_series}{timeseries.id}"]
|
|
84
109
|
|
|
85
110
|
# Set rdf type
|
|
86
111
|
type_ = self._get_rdf_type(timeseries)
|
|
@@ -158,7 +183,7 @@ class TimeSeriesExtractor(ClassicCDFExtractor[TimeSeries]):
|
|
|
158
183
|
(
|
|
159
184
|
id_,
|
|
160
185
|
self.namespace.dataset,
|
|
161
|
-
self.namespace[f"
|
|
186
|
+
self.namespace[f"{InstanceIdPrefix.data_set}{timeseries.data_set_id}"],
|
|
162
187
|
)
|
|
163
188
|
)
|
|
164
189
|
|
|
@@ -167,7 +192,7 @@ class TimeSeriesExtractor(ClassicCDFExtractor[TimeSeries]):
|
|
|
167
192
|
(
|
|
168
193
|
id_,
|
|
169
194
|
self.namespace.asset,
|
|
170
|
-
self.namespace[f"
|
|
195
|
+
self.namespace[f"{InstanceIdPrefix.asset}{timeseries.asset_id}"],
|
|
171
196
|
)
|
|
172
197
|
)
|
|
173
198
|
|
|
@@ -202,13 +202,13 @@ class DexpiExtractor(BaseExtractor):
|
|
|
202
202
|
|
|
203
203
|
@classmethod
|
|
204
204
|
def _get_element_label(cls, element: Element) -> str | None:
|
|
205
|
-
if children := get_children(element, "Label", 1):
|
|
206
|
-
if grandchildren := get_children(children[0], "Text", 1):
|
|
205
|
+
if children := get_children(element, "Label", no_children=1):
|
|
206
|
+
if grandchildren := get_children(children[0], "Text", no_children=1):
|
|
207
207
|
if "String" in grandchildren[0].attrib:
|
|
208
208
|
return grandchildren[0].attrib["String"]
|
|
209
209
|
|
|
210
210
|
# extension for schema version 3.3, where text is used to "label" without a <label> parent
|
|
211
|
-
elif children := get_children(element, "Text", 1):
|
|
211
|
+
elif children := get_children(element, "Text", no_children=1):
|
|
212
212
|
if "String" in children[0].attrib:
|
|
213
213
|
return children[0].attrib["String"]
|
|
214
214
|
|
|
@@ -219,7 +219,7 @@ class DexpiExtractor(BaseExtractor):
|
|
|
219
219
|
# TODO: This requires more work as there are multiple groupings of GenericAttributes
|
|
220
220
|
|
|
221
221
|
attributes = defaultdict(list)
|
|
222
|
-
if children := get_children(element, "GenericAttributes", 1):
|
|
222
|
+
if children := get_children(element, "GenericAttributes", no_children=1):
|
|
223
223
|
if grandchildren := get_children(children[0], "GenericAttribute"):
|
|
224
224
|
for generic_attribute in grandchildren:
|
|
225
225
|
# extension for schema version 3.3, where "AttributeURI" is not included
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import xml.etree.ElementTree as ET
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
from xml.etree.ElementTree import Element
|
|
5
|
+
|
|
6
|
+
from rdflib import RDF, Literal, Namespace, URIRef
|
|
7
|
+
|
|
8
|
+
from cognite.neat.constants import DEFAULT_NAMESPACE
|
|
9
|
+
from cognite.neat.graph.extractors._base import BaseExtractor
|
|
10
|
+
from cognite.neat.graph.models import Triple
|
|
11
|
+
from cognite.neat.utils.rdf_ import as_neat_compliant_uri
|
|
12
|
+
from cognite.neat.utils.text import to_camel
|
|
13
|
+
from cognite.neat.utils.xml_ import get_children
|
|
14
|
+
|
|
15
|
+
IODD = Namespace("http://www.io-link.com/IODD/2010/10/")
|
|
16
|
+
XSI = Namespace("http://www.w3.org/2001/XMLSchema-instance/")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IODDExtractor(BaseExtractor):
|
|
20
|
+
"""
|
|
21
|
+
IODD-XML extractor of RDF triples
|
|
22
|
+
|
|
23
|
+
Each IODD sheet describes an IODD device. This extractor extracts rdf triples that describes the device, and the
|
|
24
|
+
sensors connected to the device.
|
|
25
|
+
This data is described under the elements "DeviceIdentity" and "ProcessDataCollection".
|
|
26
|
+
In addition, triples extacted from "DeviceIdentity" and
|
|
27
|
+
"ProcessDataCollection" may reference "Text" elements which are found under "ExternalTextCollection". Edges to
|
|
28
|
+
these Text element nodes are also extracted.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
root: XML root element of IODD XML file.
|
|
32
|
+
namespace: Optional custom namespace to use for extracted triples that define data
|
|
33
|
+
model instances. Defaults to DEFAULT_NAMESPACE.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
device_elements_with_text_nodes: ClassVar[list[str]] = ["VendorText", "VendorUrl", "DeviceName", "DeviceFamily"]
|
|
37
|
+
|
|
38
|
+
def __init__(self, root: Element, namespace: Namespace | None = None):
|
|
39
|
+
self.root = root
|
|
40
|
+
self.namespace = namespace or DEFAULT_NAMESPACE
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_file(cls, filepath: str | Path, namespace: Namespace | None = None):
|
|
44
|
+
return cls(ET.parse(filepath).getroot(), namespace)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def _from_root2triples(cls, root: Element, namespace: Namespace) -> list[Triple]:
|
|
48
|
+
"""Loops through the relevant elements of the IODD XML sheet to create rdf triples that describes the IODD
|
|
49
|
+
device by starting at the root element.
|
|
50
|
+
"""
|
|
51
|
+
triples: list[Triple] = []
|
|
52
|
+
|
|
53
|
+
# Extract DeviceIdentity triples
|
|
54
|
+
if di_root := get_children(
|
|
55
|
+
root, "DeviceIdentity", ignore_namespace=True, include_nested_children=True, no_children=1
|
|
56
|
+
):
|
|
57
|
+
triples.extend(cls._iodd_device_identity2triples(di_root[0], namespace))
|
|
58
|
+
|
|
59
|
+
# Extract ProcessDataCollection triples -
|
|
60
|
+
# this element holds the information about the sensors with data coming from MQTT
|
|
61
|
+
if pc_root := get_children(
|
|
62
|
+
root, "ProcessDataCollection", ignore_namespace=True, include_nested_children=True, no_children=1
|
|
63
|
+
):
|
|
64
|
+
triples.extend(cls._process_data_collection2triples(pc_root[0], namespace))
|
|
65
|
+
|
|
66
|
+
if et_root := get_children(
|
|
67
|
+
root, "ExternalTextCollection", ignore_namespace=True, include_nested_children=True, no_children=1
|
|
68
|
+
):
|
|
69
|
+
triples.extend(cls._text_elements2triples(et_root[0], namespace))
|
|
70
|
+
|
|
71
|
+
return triples
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def _device_2text_elements_edges(cls, di_root: Element, id: URIRef, namespace: Namespace) -> list[Triple]:
|
|
75
|
+
"""
|
|
76
|
+
Create edges from the device node to text nodes.
|
|
77
|
+
"""
|
|
78
|
+
triples: list[Triple] = []
|
|
79
|
+
|
|
80
|
+
for element_tag in cls.device_elements_with_text_nodes:
|
|
81
|
+
if child := get_children(
|
|
82
|
+
di_root, child_tag=element_tag, ignore_namespace=True, include_nested_children=True, no_children=1
|
|
83
|
+
):
|
|
84
|
+
if text_id := child[0].attrib.get("textId"):
|
|
85
|
+
# Create connection from device to textId node
|
|
86
|
+
element_tag = to_camel(element_tag)
|
|
87
|
+
triples.append((id, IODD[element_tag], namespace[text_id]))
|
|
88
|
+
|
|
89
|
+
return triples
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def _text_elements2triples(cls, et_root: Element, namespace: Namespace) -> list[Triple]:
|
|
93
|
+
"""
|
|
94
|
+
This method extracts all text item triples under the ExternalTextCollection element. This will create a node
|
|
95
|
+
for each text item, and add the text value as a property to the node.
|
|
96
|
+
"""
|
|
97
|
+
triples: list[Triple] = []
|
|
98
|
+
|
|
99
|
+
if text_elements := get_children(
|
|
100
|
+
et_root, child_tag="Text", ignore_namespace=True, include_nested_children=True
|
|
101
|
+
):
|
|
102
|
+
for element in text_elements:
|
|
103
|
+
if id := element.attrib.get("id"):
|
|
104
|
+
text_id = namespace[id]
|
|
105
|
+
|
|
106
|
+
# Create Text node
|
|
107
|
+
triples.append((text_id, RDF.type, as_neat_compliant_uri(IODD["Text"])))
|
|
108
|
+
|
|
109
|
+
# Resolve text value related to the text item
|
|
110
|
+
if value := element.attrib.get("value"):
|
|
111
|
+
triples.append((text_id, IODD.value, Literal(value)))
|
|
112
|
+
|
|
113
|
+
return triples
|
|
114
|
+
|
|
115
|
+
# TODO
|
|
116
|
+
@classmethod
|
|
117
|
+
def _process_data_collection2triples(cls, pc_root: Element, namespace: Namespace) -> list[Triple]:
|
|
118
|
+
"""
|
|
119
|
+
ProcessDataCollection contains both ProcessDataIn and ProcessDataOut. Here, we are interested in the elements
|
|
120
|
+
that corresponds to actual sensor values that we will rececive from the MQTT connection.
|
|
121
|
+
|
|
122
|
+
Most likely this is ProcessDataOut elements (to be verified).
|
|
123
|
+
"""
|
|
124
|
+
triples: list[Triple] = []
|
|
125
|
+
|
|
126
|
+
return triples
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def _iodd_device_identity2triples(cls, di_root: Element, namespace: Namespace) -> list[Triple]:
|
|
130
|
+
"""
|
|
131
|
+
Properties and metadata related to the IO Device are described under the 'DeviceIdentity' element in the XML.
|
|
132
|
+
This method extracts the triples that describe the device's identity which is found under the
|
|
133
|
+
DeviceIdentity element and its child elements.
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
triples: list[Triple] = []
|
|
137
|
+
|
|
138
|
+
if device_id := di_root.attrib.get("deviceId"):
|
|
139
|
+
id_ = namespace[device_id]
|
|
140
|
+
|
|
141
|
+
for attribute_name, attribute_value in di_root.attrib.items():
|
|
142
|
+
if attribute_name == "deviceId":
|
|
143
|
+
# Create rdf type triple for IODD
|
|
144
|
+
triples.append(
|
|
145
|
+
(
|
|
146
|
+
id_,
|
|
147
|
+
RDF.type,
|
|
148
|
+
as_neat_compliant_uri(IODD["IoddDevice"]),
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
# Collect attributes at root element
|
|
153
|
+
triples.append((id_, IODD[attribute_name], Literal(attribute_value)))
|
|
154
|
+
|
|
155
|
+
triples.extend(cls._device_2text_elements_edges(di_root, id_, namespace))
|
|
156
|
+
return triples
|
|
157
|
+
|
|
158
|
+
def extract(self) -> list[Triple]:
|
|
159
|
+
# Extract triples from IODD device XML
|
|
160
|
+
return self._from_root2triples(self.root, self.namespace)
|
cognite/neat/issues/_base.py
CHANGED
|
@@ -190,6 +190,10 @@ class NeatError(NeatIssue, Exception):
|
|
|
190
190
|
read_info_by_sheet = kwargs.get("read_info_by_sheet")
|
|
191
191
|
|
|
192
192
|
for error in errors:
|
|
193
|
+
if error["type"] == "is_instance_of" and error["loc"][1] == "is-instance[SheetList]":
|
|
194
|
+
# Skip the error for SheetList, as it is not relevant for the user. This is an
|
|
195
|
+
# internal class used to have helper methods for a lists as .to_pandas()
|
|
196
|
+
continue
|
|
193
197
|
ctx = error.get("ctx")
|
|
194
198
|
if isinstance(ctx, dict) and isinstance(multi_error := ctx.get("error"), MultiValueError):
|
|
195
199
|
if read_info_by_sheet:
|
|
@@ -363,10 +367,10 @@ class NeatIssueList(UserList[T_NeatIssue], Sequence[T_NeatIssue], ABC):
|
|
|
363
367
|
"""Return all the warnings in this list."""
|
|
364
368
|
return type(self)([issue for issue in self if isinstance(issue, NeatWarning)]) # type: ignore[misc]
|
|
365
369
|
|
|
366
|
-
def as_errors(self) -> ExceptionGroup:
|
|
370
|
+
def as_errors(self, operation: str = "Operation failed") -> ExceptionGroup:
|
|
367
371
|
"""Return an ExceptionGroup with all the errors in this list."""
|
|
368
372
|
return ExceptionGroup(
|
|
369
|
-
|
|
373
|
+
operation,
|
|
370
374
|
[issue for issue in self if isinstance(issue, NeatError)],
|
|
371
375
|
)
|
|
372
376
|
|
|
@@ -17,7 +17,7 @@ from cognite.neat.rules.models import (
|
|
|
17
17
|
DataModelType,
|
|
18
18
|
ExtensionCategory,
|
|
19
19
|
SchemaCompleteness,
|
|
20
|
-
|
|
20
|
+
SheetRow,
|
|
21
21
|
)
|
|
22
22
|
from cognite.neat.rules.models.dms import DMSMetadata
|
|
23
23
|
from cognite.neat.rules.models.domain import DomainMetadata
|
|
@@ -210,14 +210,14 @@ class ExcelExporter(BaseExporter[VerifiedRules, Workbook]):
|
|
|
210
210
|
cell.font = Font(bold=True, size=12)
|
|
211
211
|
|
|
212
212
|
@classmethod
|
|
213
|
-
def _get_item_class(cls, annotation: GenericAlias) -> type[
|
|
213
|
+
def _get_item_class(cls, annotation: GenericAlias) -> type[SheetRow]:
|
|
214
214
|
if not isinstance(annotation, GenericAlias):
|
|
215
215
|
raise ValueError(f"Expected annotation to be a GenericAlias, but got {type(annotation)}")
|
|
216
216
|
args = get_args(annotation)
|
|
217
217
|
if len(args) != 1:
|
|
218
218
|
raise ValueError(f"Expected annotation to have exactly one argument, but got {len(args)}")
|
|
219
219
|
arg = args[0]
|
|
220
|
-
if not issubclass(arg,
|
|
220
|
+
if not issubclass(arg, SheetRow):
|
|
221
221
|
raise ValueError(f"Expected annotation to have a BaseModel argument, but got {type(arg)}")
|
|
222
222
|
return arg
|
|
223
223
|
|
|
@@ -58,6 +58,10 @@ class YAMLExporter(BaseExporter[VerifiedRules, str]):
|
|
|
58
58
|
def export(self, rules: VerifiedRules) -> str:
|
|
59
59
|
"""Export rules to YAML (or JSON) format.
|
|
60
60
|
|
|
61
|
+
This will export the rules to YAML format if the output is set to "yaml" and JSON format if the output is set.
|
|
62
|
+
All None and Unset values are excluded from the output to keep the output clean, i.e., only the values the user
|
|
63
|
+
has set.
|
|
64
|
+
|
|
61
65
|
Args:
|
|
62
66
|
rules: The rules to be exported.
|
|
63
67
|
|
|
@@ -66,7 +70,7 @@ class YAMLExporter(BaseExporter[VerifiedRules, str]):
|
|
|
66
70
|
"""
|
|
67
71
|
# model_dump_json ensures that the output is in JSON format,
|
|
68
72
|
# if we don't do this, we will get Enums and other types that are not serializable to YAML
|
|
69
|
-
json_output = rules.dump(mode="json")
|
|
73
|
+
json_output = rules.dump(mode="json", exclude_none=True, exclude_unset=True)
|
|
70
74
|
if self.output == "json":
|
|
71
75
|
return json.dumps(json_output)
|
|
72
76
|
elif self.output == "yaml":
|
|
@@ -4,7 +4,7 @@ from cognite.neat.rules.models.domain import DomainInputRules, DomainRules
|
|
|
4
4
|
from cognite.neat.rules.models.information._rules import InformationRules
|
|
5
5
|
from cognite.neat.rules.models.information._rules_input import InformationInputRules
|
|
6
6
|
|
|
7
|
-
from ._base_rules import DataModelType, ExtensionCategory, RoleTypes, SchemaCompleteness,
|
|
7
|
+
from ._base_rules import DataModelType, ExtensionCategory, RoleTypes, SchemaCompleteness, SheetList, SheetRow
|
|
8
8
|
from .dms._rules import DMSRules
|
|
9
9
|
from .dms._rules_input import DMSInputRules
|
|
10
10
|
from .dms._schema import DMSSchema
|
|
@@ -42,5 +42,5 @@ __all__ = [
|
|
|
42
42
|
"ExtensionCategory",
|
|
43
43
|
"DataModelType",
|
|
44
44
|
"SheetList",
|
|
45
|
-
"
|
|
45
|
+
"SheetRow",
|
|
46
46
|
]
|
|
@@ -18,7 +18,7 @@ from dataclasses import Field, dataclass, fields, is_dataclass
|
|
|
18
18
|
from types import GenericAlias, UnionType
|
|
19
19
|
from typing import Any, Generic, TypeVar, Union, cast, get_args, get_origin, overload
|
|
20
20
|
|
|
21
|
-
from ._base_rules import BaseRules,
|
|
21
|
+
from ._base_rules import BaseRules, SchemaModel
|
|
22
22
|
|
|
23
23
|
if sys.version_info >= (3, 11):
|
|
24
24
|
from typing import Self
|
|
@@ -26,7 +26,7 @@ else:
|
|
|
26
26
|
from typing_extensions import Self
|
|
27
27
|
|
|
28
28
|
T_BaseRules = TypeVar("T_BaseRules", bound=BaseRules)
|
|
29
|
-
T_RuleModel = TypeVar("T_RuleModel", bound=
|
|
29
|
+
T_RuleModel = TypeVar("T_RuleModel", bound=SchemaModel)
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
@dataclass
|