cognite-neat 0.119.2__py3-none-any.whl → 0.119.4__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 (30) hide show
  1. cognite/neat/_constants.py +34 -70
  2. cognite/neat/_graph/extractors/__init__.py +0 -6
  3. cognite/neat/_graph/loaders/_rdf2dms.py +5 -5
  4. cognite/neat/_graph/queries/__init__.py +1 -1
  5. cognite/neat/_graph/queries/_base.py +2 -456
  6. cognite/neat/_graph/queries/_queries.py +16 -0
  7. cognite/neat/_graph/queries/_select.py +440 -0
  8. cognite/neat/_graph/queries/_update.py +37 -0
  9. cognite/neat/_rules/exporters/_rules2excel.py +240 -107
  10. cognite/neat/_rules/models/_base_rules.py +16 -1
  11. cognite/neat/_rules/models/dms/_validation.py +10 -1
  12. cognite/neat/_rules/transformers/_converters.py +16 -6
  13. cognite/neat/_session/_drop.py +2 -2
  14. cognite/neat/_session/_explore.py +4 -4
  15. cognite/neat/_session/_prepare.py +5 -5
  16. cognite/neat/_session/_read.py +6 -0
  17. cognite/neat/_session/_set.py +3 -3
  18. cognite/neat/_session/_show.py +1 -1
  19. cognite/neat/_session/_template.py +24 -5
  20. cognite/neat/_state/README.md +23 -0
  21. cognite/neat/_store/_graph_store.py +38 -39
  22. cognite/neat/_version.py +1 -1
  23. {cognite_neat-0.119.2.dist-info → cognite_neat-0.119.4.dist-info}/METADATA +37 -2
  24. {cognite_neat-0.119.2.dist-info → cognite_neat-0.119.4.dist-info}/RECORD +27 -26
  25. cognite/neat/_graph/extractors/_dexpi.py +0 -234
  26. cognite/neat/_graph/extractors/_iodd.py +0 -403
  27. cognite/neat/_graph/transformers/_iodd.py +0 -30
  28. {cognite_neat-0.119.2.dist-info → cognite_neat-0.119.4.dist-info}/LICENSE +0 -0
  29. {cognite_neat-0.119.2.dist-info → cognite_neat-0.119.4.dist-info}/WHEEL +0 -0
  30. {cognite_neat-0.119.2.dist-info → cognite_neat-0.119.4.dist-info}/entry_points.txt +0 -0
@@ -1,234 +0,0 @@
1
- import xml.etree.ElementTree as ET
2
- from collections import defaultdict
3
- from collections.abc import Iterable
4
- from pathlib import Path
5
- from xml.etree.ElementTree import Element
6
-
7
- from rdflib import RDF, RDFS, XSD, Literal, Namespace, URIRef
8
- from typing_extensions import Self
9
-
10
- from cognite.neat._constants import DEFAULT_NAMESPACE
11
- from cognite.neat._graph.extractors._base import BaseExtractor
12
- from cognite.neat._shared import Triple
13
- from cognite.neat._utils.rdf_ import as_neat_compliant_uri
14
- from cognite.neat._utils.xml_ import get_children, iterate_tree
15
-
16
- DEXPI = Namespace("http://sandbox.dexpi.org/rdl/")
17
- QUDT = Namespace("https://qudt.org/vocab/unit/")
18
-
19
-
20
- class DexpiExtractor(BaseExtractor):
21
- """
22
- DEXPI-XML extractor of RDF triples
23
-
24
- Args:
25
- root: XML root element of DEXPI file.
26
- namespace: Optional custom namespace to use for extracted triples that define data
27
- model instances. Defaults to DEFAULT_NAMESPACE.
28
- """
29
-
30
- def __init__(
31
- self,
32
- root: Element,
33
- namespace: Namespace | None = None,
34
- ):
35
- self.root = root
36
- self.namespace = namespace or DEFAULT_NAMESPACE
37
-
38
- @classmethod
39
- def from_file(cls, filepath: str | Path, namespace: Namespace | None = None) -> Self:
40
- return cls(ET.parse(filepath).getroot(), namespace)
41
-
42
- @classmethod
43
- def from_url(cls, url: str, namespace: Namespace | None = None) -> Self:
44
- from io import BytesIO
45
-
46
- import requests
47
-
48
- response = requests.get(url)
49
- response.raise_for_status()
50
- return cls(ET.parse(BytesIO(response.content)).getroot(), namespace)
51
-
52
- def extract(self) -> Iterable[Triple]:
53
- """Extracts RDF triples from DEXPI XML file."""
54
-
55
- for element in iterate_tree(self.root):
56
- yield from self._element2triples(element, self.namespace)
57
-
58
- @classmethod
59
- def _element2triples(cls, element: Element, namespace: Namespace) -> list[Triple]:
60
- """Converts an element to triples."""
61
- triples: list[Triple] = []
62
-
63
- if (
64
- "ComponentClass" in element.attrib
65
- and element.attrib["ComponentClass"] != "Label"
66
- and "ID" in element.attrib
67
- ):
68
- id_ = namespace[element.attrib["ID"]]
69
-
70
- if node_triples := cls._element2node_triples(id_, element):
71
- triples.extend(node_triples)
72
-
73
- if edge_triples := cls._element2edge_triples(id_, element, namespace):
74
- triples.extend(edge_triples)
75
-
76
- return triples
77
-
78
- @classmethod
79
- def _element2edge_triples(cls, id_: URIRef, element: Element, namespace: Namespace) -> list[Triple]:
80
- triples: list[Triple] = []
81
-
82
- # connection triples
83
- if connections := get_children(element, "Connection"):
84
- for connection in connections:
85
- if "FromID" in connection.attrib and "ToID" in connection.attrib:
86
- triples.append(
87
- (
88
- namespace[connection.attrib["FromID"]],
89
- DEXPI.connection,
90
- namespace[connection.attrib["ToID"]],
91
- )
92
- )
93
-
94
- # association triples
95
- if associations := get_children(element, "Association"):
96
- for association in associations:
97
- if "Type" in association.attrib and "ItemID" in association.attrib:
98
- association_type = cls._to_uri_friendly_association_type(association)
99
-
100
- triples.append(
101
- (
102
- id_,
103
- DEXPI[f"association/{association_type}"],
104
- namespace[association.attrib["ItemID"]],
105
- )
106
- )
107
-
108
- # children-parent triples
109
- for child in element:
110
- if "ID" in child.attrib and child.tag != "Label":
111
- camel_case_property = child.tag[0].lower() + child.tag[1:]
112
- triples.append(
113
- (
114
- id_,
115
- DEXPI[f"children/{camel_case_property}"],
116
- namespace[child.attrib["ID"]],
117
- )
118
- )
119
-
120
- return triples
121
-
122
- @classmethod
123
- def _to_uri_friendly_association_type(cls, association: Element) -> str:
124
- association_type = "".join(
125
- [word.capitalize() if i != 0 else word for i, word in enumerate(association.attrib["Type"].split(" "))]
126
- )
127
-
128
- return association_type
129
-
130
- @classmethod
131
- def _element2node_triples(cls, id_: URIRef, element: Element) -> list[Triple]:
132
- """Converts an XML element to triples."""
133
- triples: list[Triple] = []
134
-
135
- # setting these to None this is the order of getting the type
136
- component_class: str | None = None
137
- component_name: str | None = None
138
- tag: str | None = None
139
-
140
- # adding tag triple if exists
141
- if tag := element.tag:
142
- triples.append((id_, DEXPI.tag, Literal(str(tag))))
143
-
144
- # adding attributes triples
145
- if attributes := element.attrib:
146
- if component_class := attributes.get("ComponentClass", None):
147
- triples.append((id_, DEXPI.ComponentClass, Literal(component_class)))
148
- if component_name := attributes.get("ComponentName", None):
149
- triples.append((id_, DEXPI.ComponentName, Literal(component_name)))
150
- if component_class_uri := attributes.get("ComponentClassURI", None):
151
- triples.append((id_, DEXPI.ComponentClassURI, URIRef(component_class_uri)))
152
-
153
- triples.append(
154
- (
155
- id_,
156
- RDF.type,
157
- as_neat_compliant_uri(DEFAULT_NAMESPACE[component_class or component_name or tag or "Unknown"]),
158
- )
159
- )
160
-
161
- # add label triple
162
- if label := cls._get_element_label(element):
163
- triples.append((id_, RDFS.label, Literal(label)))
164
-
165
- # add generic attributes triples
166
- if generic_attributes := cls._get_element_generic_attributes(element):
167
- for attribute, value_definitions in generic_attributes.items():
168
- predicate = as_neat_compliant_uri(attribute)
169
- for value_definition in value_definitions:
170
- if literal := cls._value_definition2literal(value_definition):
171
- triples.append((id_, predicate, literal))
172
-
173
- return triples
174
-
175
- @classmethod
176
- def _value_definition2literal(cls, definition: dict, make_unit_datatype: bool = False) -> Literal | None:
177
- if "Value" not in definition or "Format" not in definition:
178
- return None
179
-
180
- if "Units" in definition and "Value" in definition:
181
- if make_unit_datatype and "UnitsURI" in definition:
182
- return Literal(definition["Value"], datatype=URIRef(definition["UnitsURI"]))
183
-
184
- else:
185
- return Literal(definition["Value"], datatype=XSD.float)
186
-
187
- # case: when language is present we create add language tag to the literal
188
- elif "Language" in definition and "Value" in definition:
189
- return Literal(definition["Value"], lang=definition["Language"])
190
-
191
- # case: when ValueURI is present we use it instead of Value
192
- # this would be candidate for ENUMs in CDF
193
- elif "ValueURI" in definition:
194
- return Literal(definition["ValueURI"], datatype=XSD[definition["Format"]])
195
-
196
- # case: when Format is not string we make sure to add the datatype
197
- elif definition["Format"].lower() != "string":
198
- return Literal(definition["Value"], datatype=XSD[definition["Format"]])
199
-
200
- # case: when Format is string we add the literal without datatype (easier to read triples, less noise)
201
- else:
202
- return Literal(definition["Value"])
203
-
204
- @classmethod
205
- def _get_element_label(cls, element: Element) -> str | None:
206
- if children := get_children(element, "Label", no_children=1):
207
- if grandchildren := get_children(children[0], "Text", no_children=1):
208
- if "String" in grandchildren[0].attrib:
209
- return grandchildren[0].attrib["String"]
210
-
211
- # extension for schema version 3.3, where text is used to "label" without a <label> parent
212
- elif children := get_children(element, "Text", no_children=1):
213
- if "String" in children[0].attrib:
214
- return children[0].attrib["String"]
215
-
216
- return None
217
-
218
- @classmethod
219
- def _get_element_generic_attributes(cls, element: Element) -> dict:
220
- # TODO: This requires more work as there are multiple groupings of GenericAttributes
221
-
222
- attributes = defaultdict(list)
223
- if children := get_children(element, "GenericAttributes", no_children=1):
224
- if grandchildren := get_children(children[0], "GenericAttribute"):
225
- for generic_attribute in grandchildren:
226
- # extension for schema version 3.3, where "AttributeURI" is not included
227
- if name := generic_attribute.attrib.get("Name", None):
228
- attribute_uri = as_neat_compliant_uri(DEFAULT_NAMESPACE[name])
229
- if attribute_uri not in attributes:
230
- attributes[attribute_uri] = [generic_attribute.attrib]
231
- else:
232
- attributes[attribute_uri].append(generic_attribute.attrib)
233
-
234
- return attributes
@@ -1,403 +0,0 @@
1
- import re
2
- import uuid
3
- import xml.etree.ElementTree as ET
4
- from functools import cached_property
5
- from pathlib import Path
6
- from typing import ClassVar
7
- from typing import Literal as LiteralType
8
- from xml.etree.ElementTree import Element
9
-
10
- from rdflib import RDF, XSD, Literal, Namespace, URIRef
11
- from typing_extensions import Self
12
-
13
- from cognite.neat._constants import DEFAULT_NAMESPACE
14
- from cognite.neat._graph.extractors._base import BaseExtractor
15
- from cognite.neat._issues.errors import FileReadError, NeatValueError
16
- from cognite.neat._shared import Triple
17
- from cognite.neat._utils.rdf_ import remove_namespace_from_uri
18
- from cognite.neat._utils.text import to_camel_case
19
- from cognite.neat._utils.xml_ import get_children
20
-
21
- IODD = Namespace("http://www.io-link.com/IODD/2010/10/")
22
- XSI = Namespace("http://www.w3.org/2001/XMLSchema-instance/")
23
-
24
- XSI_XML_PREFIX = "{http://www.w3.org/2001/XMLSchema-instance}"
25
-
26
-
27
- class IODDExtractor(BaseExtractor):
28
- """
29
- IODD-XML extractor of RDF triples
30
-
31
- Each IODD sheet describes an IODD device. This extractor extracts rdf triples that describes the device, and the
32
- sensors connected to the device.
33
- This data is described under the elements "DeviceIdentity" and "ProcessDataCollection".
34
- In addition, triples extacted from "DeviceIdentity" and
35
- "ProcessDataCollection" may reference "Text" elements which are found under "ExternalTextCollection". Edges to
36
- these Text element nodes are also extracted.
37
-
38
- Args:
39
- root: XML root element of IODD XML file.
40
- namespace: Optional custom namespace to use for extracted triples that define data
41
- model instances. Defaults to DEFAULT_NAMESPACE.
42
- device_id: Optional user specified unique id/tag for actual equipment instance. If not provided, a randomly
43
- generated UUID will be used. The device_id must be WEB compliant,
44
- meaning that the characters /&?=: % are not allowed
45
- """
46
-
47
- device_elements_with_text_nodes: ClassVar[list[str]] = ["VendorText", "VendorUrl", "DeviceName", "DeviceFamily"]
48
- std_variable_elements_to_extract: ClassVar[list[str]] = ["V_SerialNumber", "V_ApplicationSpecificTag"]
49
- text_elements_language: LiteralType["en", "de"] = "en"
50
-
51
- def __init__(
52
- self,
53
- root: Element,
54
- namespace: Namespace | None = None,
55
- device_id: str | None = None,
56
- ):
57
- self.root = root
58
- self.namespace = namespace or DEFAULT_NAMESPACE
59
-
60
- if device_id and device_id != re.sub(r"[^a-zA-Z0-9-_.]", "", device_id):
61
- raise NeatValueError("Specified device_id is not web compliant. Please exclude characters: /&?=: %")
62
-
63
- self.device_id = (
64
- self.namespace[device_id] if device_id else self.namespace[f"Device_{str(uuid.uuid4()).replace('-', '_')}"]
65
- )
66
-
67
- @cached_property
68
- def _text_id_2value_mapping(self) -> dict[str, str]:
69
- """
70
- !!! note used for "Prototype Solution" !!!
71
- A mapping for text_id references to Text elements under ExternalTextCollection.
72
- The mapping can be used to find the Text element with matching id, and returns
73
- the value associated with the Text element.
74
- """
75
- mapping = {}
76
- if et_root := get_children(
77
- self.root, "ExternalTextCollection", ignore_namespace=True, include_nested_children=True, no_children=1
78
- ):
79
- if language_element := get_children(et_root[0], "PrimaryLanguage", ignore_namespace=True, no_children=1):
80
- if (
81
- language_element[0].attrib.get("{http://www.w3.org/XML/1998/namespace}lang")
82
- == self.text_elements_language
83
- ):
84
- if text_elements := get_children(
85
- language_element[0], child_tag="Text", ignore_namespace=True, include_nested_children=True
86
- ):
87
- for element in text_elements:
88
- if id := element.attrib.get("id"):
89
- if value := element.attrib.get("value"):
90
- mapping[id] = value
91
- return mapping
92
-
93
- @classmethod
94
- def from_file(cls, filepath: Path, namespace: Namespace | None = None, device_id: str | None = None) -> Self:
95
- if filepath.suffix != ".xml":
96
- raise FileReadError(filepath, "File is not XML.")
97
- return cls(ET.parse(filepath).getroot(), namespace, device_id)
98
-
99
- @classmethod
100
- def _from_root2triples(cls, root: Element, namespace: Namespace, device_id: URIRef) -> list[Triple]:
101
- """Loops through the relevant elements of the IODD XML sheet to create rdf triples that describes the IODD
102
- device by starting at the root element.
103
- """
104
- triples: list[Triple] = []
105
-
106
- # Extract DeviceIdentity triples
107
- if di_root := get_children(
108
- root, "DeviceIdentity", ignore_namespace=True, include_nested_children=True, no_children=1
109
- ):
110
- triples.extend(cls._iodd_device_identity2triples(di_root[0], namespace, device_id))
111
-
112
- # Extract VariableCollection triples -
113
- # this element holds the information about the sensors connected to the device that collects data such as
114
- # temperature, voltage, leakage etc.
115
- if vc_root := get_children(
116
- root, "VariableCollection", ignore_namespace=True, include_nested_children=True, no_children=1
117
- ):
118
- triples.extend(cls._variables_data_collection2triples(vc_root[0], namespace, device_id))
119
-
120
- if pc_root := get_children(
121
- root, "ProcessDataCollection", ignore_namespace=True, include_nested_children=True, no_children=1
122
- ):
123
- triples.extend(cls._process_data_collection2triples(pc_root[0], namespace, device_id))
124
-
125
- if et_root := get_children(
126
- root, "ExternalTextCollection", ignore_namespace=True, include_nested_children=True, no_children=1
127
- ):
128
- triples.extend(cls._text_elements2triples(et_root[0], namespace))
129
-
130
- return triples
131
-
132
- @classmethod
133
- def _process_data_collection2triples(
134
- cls, pc_root: Element, namespace: Namespace, device_id: URIRef
135
- ) -> list[Triple]:
136
- """
137
- Will only collect ProcessDataIn elements at this point. The data from the IO-master is transmitted as an
138
- array related to a ProcessDataIn item.
139
- """
140
- triples: list[Triple] = []
141
-
142
- if process_data_in := get_children(
143
- pc_root, "ProcessDataIn", ignore_namespace=True, include_nested_children=True
144
- ):
145
- for process_data_element in process_data_in:
146
- if p_id := process_data_element.attrib.get("id"):
147
- device_id_str = remove_namespace_from_uri(device_id)
148
- process_data_in_id = namespace[f"{device_id_str}.{p_id}"]
149
-
150
- # Create ProcessDataIn node
151
- triples.append((process_data_in_id, RDF.type, IODD.ProcessDataIn))
152
-
153
- # Create connection from device to node
154
- triples.append((device_id, IODD.processDataIn, process_data_in_id))
155
-
156
- # Connect record items (essentially an array of indexed variables) to the ProcessDataIn node
157
- triples.extend(cls._process_data_in_records2triples(process_data_element, process_data_in_id))
158
-
159
- return triples
160
-
161
- @classmethod
162
- def _device_2text_elements_edges(cls, di_root: Element, id: URIRef, namespace: Namespace) -> list[Triple]:
163
- """
164
- Create edges from the device node to text nodes.
165
- """
166
- triples: list[Triple] = []
167
-
168
- for element_tag in cls.device_elements_with_text_nodes:
169
- if child := get_children(
170
- di_root, child_tag=element_tag, ignore_namespace=True, include_nested_children=True, no_children=1
171
- ):
172
- if text_id := child[0].attrib.get("textId"):
173
- # Create connection from device to textId node
174
- element_tag = to_camel_case(element_tag)
175
- triples.append((id, IODD[element_tag], namespace[text_id]))
176
-
177
- return triples
178
-
179
- @classmethod
180
- def _text_elements2triples(cls, et_root: Element, namespace: Namespace) -> list[Triple]:
181
- """
182
- This method extracts all text item triples under the ExternalTextCollection element. This will create a node
183
- for each text item, and add the text value as a property to the node.
184
- """
185
- triples: list[Triple] = []
186
-
187
- if language_element := get_children(et_root, "PrimaryLanguage", ignore_namespace=True, no_children=1):
188
- if (
189
- language_element[0].attrib.get("{http://www.w3.org/XML/1998/namespace}lang")
190
- == cls.text_elements_language
191
- ):
192
- if text_elements := get_children(
193
- language_element[0], child_tag="Text", ignore_namespace=True, include_nested_children=True
194
- ):
195
- for element in text_elements:
196
- if id := element.attrib.get("id"):
197
- text_id = namespace[id]
198
-
199
- # Create Text node
200
- triples.append((text_id, RDF.type, IODD.TextObject))
201
-
202
- # Resolve text value related to the text item
203
- if value := element.attrib.get("value"):
204
- triples.append((text_id, IODD.value, Literal(value)))
205
- return triples
206
-
207
- @classmethod
208
- def _std_variables2triples(cls, vc_root: Element, namespace: Namespace, device_id: URIRef) -> list[Triple]:
209
- """
210
- For simplicity, only extract the two items we want for this use case - V_ApplicationSpecificTag and
211
- V_SerialNumber
212
- """
213
- triples: list[Triple] = []
214
-
215
- if std_variable_elements := get_children(vc_root, child_tag="StdVariableRef", ignore_namespace=True):
216
- for element in std_variable_elements:
217
- if id := element.attrib.get("id"):
218
- if id in cls.std_variable_elements_to_extract:
219
- if object := element.attrib.get("defaultValue"):
220
- predicate = to_camel_case(id.replace("V_", ""))
221
- triples.append((device_id, IODD[predicate], Literal(object)))
222
- return triples
223
-
224
- @classmethod
225
- def _variables_data_collection2triples(
226
- cls, vc_root: Element, namespace: Namespace, device_id: URIRef
227
- ) -> list[Triple]:
228
- """
229
- VariableCollection contains elements that references Variables and StdVariables. The StdVariables
230
- can be resolved by looking up the ID in the IODD-StandardDefinitions1.1.xml sheet.
231
-
232
- The Variable elements are descriptions of the sensors collecting data for the device.
233
- """
234
- triples: list[Triple] = []
235
-
236
- # StdVariableRef elements of interest
237
- triples.extend(cls._std_variables2triples(vc_root, namespace, device_id))
238
-
239
- # Variable elements (these are the descriptions of the sensors)
240
- if variable_elements := get_children(vc_root, child_tag="Variable", ignore_namespace=True):
241
- for element in variable_elements:
242
- if id := element.attrib.get("id"):
243
- device_id_str = remove_namespace_from_uri(device_id)
244
- variable_id = f"{device_id_str}.{id}"
245
-
246
- # Create connection from device node to time series
247
- triples.append((device_id, IODD.variable, Literal(variable_id, datatype=XSD["timeseries"])))
248
-
249
- return triples
250
-
251
- @classmethod
252
- def _iodd_device_identity2triples(cls, di_root: Element, namespace: Namespace, device_id: URIRef) -> list[Triple]:
253
- """
254
- Properties and metadata related to the IO Device are described under the 'DeviceIdentity' element in the XML.
255
- This method extracts the triples that describe the device's identity which is found under the
256
- DeviceIdentity element and its child elements.
257
-
258
- """
259
- triples: list[Triple] = []
260
-
261
- # Create rdf type triple for IODD
262
- triples.append(
263
- (
264
- device_id,
265
- RDF.type,
266
- IODD.IoddDevice,
267
- )
268
- )
269
-
270
- for attribute_name, attribute_value in di_root.attrib.items():
271
- triples.append((device_id, IODD[attribute_name], Literal(attribute_value)))
272
-
273
- triples.extend(cls._device_2text_elements_edges(di_root, device_id, namespace))
274
- return triples
275
-
276
- @classmethod
277
- def _process_data_in_records2triples(cls, pc_in_root: Element, process_data_in_id: URIRef) -> list[Triple]:
278
- """
279
- Extract RecordItems related to a ProcessDataIn element. Each record item is indexed. Will use this index
280
- as the identifier for the time series in CDF.
281
- """
282
- triples: list[Triple] = []
283
-
284
- if record_items := get_children(pc_in_root, "RecordItem", ignore_namespace=True, include_nested_children=True):
285
- for record in record_items:
286
- if index := record.attrib.get("subindex"):
287
- process_id_str = remove_namespace_from_uri(process_data_in_id)
288
- record_id = f"{process_id_str}.{index}"
289
- # Create connection from device node to time series
290
- triples.append((process_data_in_id, IODD.variable, Literal(record_id, datatype=XSD["timeseries"])))
291
-
292
- return triples
293
-
294
- def extract(self) -> list[Triple]:
295
- """
296
- Extract RDF triples from IODD XML
297
- """
298
- return self._from_root2triples(self.root, self.namespace, self.device_id)
299
-
300
- def _variable2info(self, variable_element: Element) -> dict:
301
- """
302
- !!! note used for "Prototype Solution" !!!
303
- Extracts information relevant to a CDF time series type from a Variable element
304
- """
305
-
306
- variable_dict = {}
307
-
308
- if name := get_children(
309
- variable_element, child_tag="Name", ignore_namespace=True, include_nested_children=False, no_children=1
310
- ):
311
- if text_id := name[0].get("textId"):
312
- variable_dict["name"] = self._text_id_2value_mapping[text_id]
313
- if description := get_children(
314
- variable_element,
315
- child_tag="Description",
316
- ignore_namespace=True,
317
- include_nested_children=False,
318
- no_children=1,
319
- ):
320
- if text_id := description[0].get("textId"):
321
- variable_dict["description"] = self._text_id_2value_mapping[text_id]
322
- if data_type := get_children(
323
- variable_element, child_tag="Datatype", ignore_namespace=True, include_nested_children=False, no_children=1
324
- ):
325
- variable_dict["data_type"] = data_type[0].attrib[f"{XSI_XML_PREFIX}type"]
326
-
327
- return variable_dict
328
-
329
- def _process_record2info(self, record_element: Element) -> dict:
330
- """
331
- !!! note used for "Prototype Solution" !!!
332
- Extracts information relevant to a CDF time series type from a Record element
333
- """
334
- record_dict = {}
335
-
336
- if name := get_children(
337
- record_element, child_tag="Name", ignore_namespace=True, include_nested_children=False, no_children=1
338
- ):
339
- if text_id := name[0].get("textId"):
340
- record_dict["name"] = self._text_id_2value_mapping[text_id]
341
- if description := get_children(
342
- record_element, child_tag="Description", ignore_namespace=True, include_nested_children=False, no_children=1
343
- ):
344
- if text_id := description[0].get("textId"):
345
- record_dict["description"] = self._text_id_2value_mapping[text_id]
346
- if data_type := get_children(
347
- record_element,
348
- child_tag="SimpleDatatype",
349
- ignore_namespace=True,
350
- include_nested_children=False,
351
- no_children=1,
352
- ):
353
- record_dict["data_type"] = data_type[0].attrib[f"{XSI_XML_PREFIX}type"]
354
- if index := record_element.attrib.get("subindex"):
355
- record_dict["index"] = index
356
-
357
- return record_dict
358
-
359
- def _extract_enhanced_ts_information(self, json_file_path: Path) -> None:
360
- """
361
- Extract additional information like name, description and data type for Variables and ProcessDataIn
362
- record elements in the IODD. The purpose is for the result gile to be used for enhancing time series with more
363
- information when they are created in CDF.
364
-
365
- Args:
366
- json_file_path: file path for where to write the extracted information about all time series
367
- in the IODD
368
-
369
- !!! note "Prototype Solution" !!!
370
- This is an intermediate solution while better support for adding this information directly
371
- into the knowledge graph for the timeseries node type is under development.
372
- """
373
- import json
374
-
375
- ts_ext_id2_info_map = {}
376
-
377
- # Variable elements (these are the descriptions of the sensors)
378
- if variable_elements := get_children(
379
- self.root, child_tag="Variable", ignore_namespace=True, include_nested_children=True
380
- ):
381
- for element in variable_elements:
382
- if id := element.attrib.get("id"):
383
- device_id_str = remove_namespace_from_uri(self.device_id)
384
- variable_id = f"{device_id_str}.{id}"
385
- ts_ext_id2_info_map[variable_id] = self._variable2info(element)
386
-
387
- if process_data_in := get_children(
388
- self.root, "ProcessDataIn", ignore_namespace=True, include_nested_children=True
389
- ):
390
- for process_data_element in process_data_in:
391
- if p_id := process_data_element.attrib.get("id"):
392
- device_id_str = remove_namespace_from_uri(self.device_id)
393
- process_data_in_id = f"{device_id_str}.{p_id}"
394
- if record_items := get_children(
395
- process_data_element, "RecordItem", ignore_namespace=True, include_nested_children=True
396
- ):
397
- for record in record_items:
398
- if index := record.attrib.get("subindex"):
399
- process_record_id = f"{process_data_in_id}.{index}"
400
- ts_ext_id2_info_map[process_record_id] = self._process_record2info(record)
401
-
402
- with Path.open(json_file_path, "w") as fp:
403
- json.dump(ts_ext_id2_info_map, fp, indent=2)
@@ -1,30 +0,0 @@
1
- from rdflib import Namespace
2
-
3
- from cognite.neat._graph.extractors import IODDExtractor
4
-
5
- from ._prune_graph import AttachPropertyFromTargetToSource, PruneDanglingNodes
6
-
7
- IODD = Namespace("http://www.io-link.com/IODD/2010/10/")
8
-
9
-
10
- class IODDAttachPropertyFromTargetToSource(AttachPropertyFromTargetToSource):
11
- _need_changes = frozenset(
12
- {
13
- str(IODDExtractor.__name__),
14
- }
15
- )
16
-
17
- def __init__(self) -> None:
18
- super().__init__(
19
- target_node_type=IODD.TextObject,
20
- target_property=IODD.value,
21
- delete_target_node=True,
22
- namespace=IODD,
23
- )
24
-
25
-
26
- class IODDPruneDanglingNodes(PruneDanglingNodes):
27
- _need_changes = frozenset({str(IODDExtractor.__name__), str(IODDAttachPropertyFromTargetToSource.__name__)})
28
-
29
- def __init__(self) -> None:
30
- super().__init__(node_prune_types=[IODD.TextObject])