diff --git a/README.md b/README.md index 70f0eed138676e90179fcdb6b41adc6850974f71..a50f7119bb0323be304f19b1e4a4ffec603f5d1a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [](https://git-ce.rwth-aachen.de/wzl-mq-ms/forschung-lehre/lava/unified-device-interface/python/commits/master) # Python Unified Device Interface -Current stable version: 9.3.8 +Current stable version: 10.0.0 ## Installation 1. Install the WZL-UDI package via pip @@ -58,6 +58,10 @@ Funded by the Deutsche Forschungsgemeinschaft (DFG, German Research Foundation) ## Recent changes +**10.0.0** - 2024-03-21 + - the getter method of a measurement must always return a tuple of value and uncertainty quantification, if the uncertainty is not applicable None must be returned for the uncertainty + - semantic path for Observation and MeasurementResults can now be resolved + **9.3.8** - 2024-03-19 - fixed semantic serialization of integer variables diff --git a/setup.py b/setup.py index da591dcd01a36c2ad1e919a9b8e4c4a187a072a1..2ad2d8163f5678be1ec035657c783ef179202450 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() setup(name='wzl-udi', - version='9.3.8', + version='10.0.0', url='https://git-ce.rwth-aachen.de/wzl-mq-public/soil/python', project_urls={ "Bug Tracker": "https://git-ce.rwth-aachen.de/wzl-mq-public/soil/python/-/issues", diff --git a/src/http/server.py b/src/http/server.py index f78cf4a6b9d5e22b8414249722767158b6ed6281..bd859843343a4fd0963fa6e2940018297f7756f3 100644 --- a/src/http/server.py +++ b/src/http/server.py @@ -216,7 +216,7 @@ class HTTPServer(object): (rdflib.URIRef(Semantics.namespace[profilename]), Namespaces.dcterms.license, Semantics.profile_license)) item, status = None, 200 - elif resource_type == ResourceType.metadata or resource_type == ResourceType.data: + elif resource_type.is_semantic(): semantic_name = request.url.parts[-2] if request.url.parts[-1] == '' else request.url.parts[-1] item, resource_type = self.root.resolve_semantic_path(semantic_name) diff --git a/src/soil/measurement.py b/src/soil/measurement.py index 741c16681b9ffba7142dd4e60c88521161019a1a..a414e77b08a4b7e32289b2fe097d567b8411c9c9 100644 --- a/src/soil/measurement.py +++ b/src/soil/measurement.py @@ -1,5 +1,7 @@ +import asyncio import copy import datetime +import inspect import warnings from typing import Dict, Callable, List @@ -7,10 +9,11 @@ import rdflib from deprecated import deprecated from .datatype import Datatype -from .variable import Variable +from .error import ChildNotFoundException +from .variable import Variable, serialize_time from .semantics import Semantics, Namespaces from ..utils import root_logger -from ..utils.constants import HTTP_GET +from ..utils.constants import HTTP_GET, HTTP_OPTIONS from ..utils.error import SerialisationException, DeviceException from ..utils.resources import ResourceType @@ -73,6 +76,33 @@ class Measurement(Variable): return self._label if item == 'label': return self._label + if item == "value": + if method != HTTP_OPTIONS: + try: + if inspect.iscoroutinefunction(self.get): + loop = asyncio.get_event_loop() + value, covariance = loop.run_until_complete(asyncio.gather(self.get()))[0] + else: + value, covariance = self.get() + + if self._datatype == Datatype.TIME: + value = serialize_time(value) + if covariance is not None: + covariance = serialize_time(value) + elif self._datatype == Datatype.ENUM: + value = str(value) + + except Exception as e: + raise DeviceException( + 'Could not provide value of Measurement/Parameter {}: {}'.format(self.uuid, str(e)), + predecessor=e) + + Variable.check_all(self._datatype, self._dimension, self._range, value) + self._value = value + self._covariance = covariance + return value + else: + return self._value if item == 'covariance': return self._covariance # if item == 'uncertainty': @@ -188,6 +218,7 @@ class Measurement(Variable): for subject in subjects: if subject != Semantics.namespace[f'{self._semantic_name}Range']: result.remove((subject, None, None)) + elif resource_type == ResourceType.data: data_graph = rdflib.Graph() data_graph.bind('sosa', Namespaces.sosa) @@ -195,45 +226,87 @@ class Measurement(Variable): data_graph.bind('soil', Namespaces.soil) data_graph.bind('qudt', Namespaces.qudt) data_graph.bind('unit', Namespaces.unit) - observation_subject = Semantics.namespace[f'{self._semantic_name}Observation'] - - sensor_triples = list(self._metadata.triples((None, Namespaces.sosa.isObservedBy, None))) - assert len(sensor_triples) == 1 - - # create observation node - data_graph.add((observation_subject, Namespaces.rdf.type, rdflib.URIRef(Namespaces.sosa.Observation))) - data_graph.add((observation_subject, Namespaces.schema.name, rdflib.Literal(f'{self._name} Observation'))) - data_graph.add( - (observation_subject, Namespaces.sosa.observedProperty, Semantics.namespace[self._semantic_name])) - data_graph.add((observation_subject, Namespaces.sosa.hasResult, - Semantics.namespace[f'{self._semantic_name}Measurement'])) - data_graph.add((observation_subject, Namespaces.sosa.madeBySensor, sensor_triples[0][2])) - data_graph.add((observation_subject, Namespaces.schema.license, Semantics.data_license)) + data_graph.bind('earl', Namespaces.earl) + measurement_subject = Semantics.namespace[f'{self._semantic_name}Measurement'] + result_subject = Semantics.namespace[f'{self._semantic_name}MeasurementResult'] + uncertainty_subject = Semantics.namespace[f'{self._semantic_name}MeasurementUncertainty'] # create result node unit_triples = list(self._metadata.triples((None, Namespaces.qudt.applicableUnit, None))) assert len(unit_triples) == 1 - measurement_subject = Semantics.namespace[f'{self._semantic_name}Measurement'] - data_graph.add((measurement_subject, Namespaces.rdf.type, rdflib.URIRef(Namespaces.sosa.Result))) - data_graph.add((measurement_subject, Namespaces.rdf.type, rdflib.URIRef(Namespaces.soil.Measurement))) - data_graph.add((measurement_subject, Namespaces.sosa.isResultOf, observation_subject)) - data_graph.add((measurement_subject, Namespaces.qudt.unit, unit_triples[0][2])) - data_graph.add((measurement_subject, Namespaces.schema.license, Semantics.data_license)) + data_graph.add((result_subject, Namespaces.rdf.type, rdflib.URIRef(Namespaces.sosa.Result))) + data_graph.add((result_subject, Namespaces.sosa.isResultOf, measurement_subject)) + data_graph.add((result_subject, Namespaces.qudt.unit, unit_triples[0][2])) + data_graph.add((result_subject, Namespaces.schema.license, Semantics.data_license)) rdf_value = self.serialize_value(data_graph, self.__getitem__('value', 0)) + data_graph.add((result_subject, Namespaces.qudt.value, rdf_value)) + data_graph.add((result_subject, Namespaces.m4i.hasUncertaintyDeclaration, uncertainty_subject)) - data_graph.add((measurement_subject, Namespaces.qudt.value, rdf_value)) - data_graph.add((measurement_subject, Namespaces.schema.dateCreated, + data_graph.add((result_subject, Namespaces.schema.dateCreated, rdflib.Literal(datetime.datetime.now().astimezone()))) - # TODO add uncertainty + result = data_graph + + elif resource_type == ResourceType.uncertainty: + data_graph = rdflib.Graph() + data_graph.bind(Semantics.prefix, Semantics.namespace) + uncertainty_subject = Semantics.namespace[f'{self._semantic_name}MeasurementUncertainty'] + covariance = self.__getitem__('covariance', 0) + print(covariance) + if covariance is not None: + rdf_covariance = self.serialize_value(data_graph, covariance) + data_graph.add((uncertainty_subject, Namespaces.rdf.type, Namespaces.si.CoverageInterval)) + data_graph.add((uncertainty_subject, Namespaces.si.hasStandardUnc, rdf_covariance)) + + else: + data_graph.bind('earl', Namespaces.earl) + data_graph.add((uncertainty_subject, Namespaces.rdf.type, Namespaces.earl.NotApplicable)) + + data_graph.add((uncertainty_subject, Namespaces.schema.license, Semantics.data_license)) + result = data_graph + + elif resource_type == ResourceType.observation: + data_graph = rdflib.Graph() + data_graph.bind('sosa', Namespaces.sosa) + data_graph.bind(Semantics.prefix, Semantics.namespace) + data_graph.bind('soil', Namespaces.soil) + data_graph.bind('qudt', Namespaces.qudt) + data_graph.bind('unit', Namespaces.unit) + measurement_subject = Semantics.namespace[f'{self._semantic_name}Measurement'] + result_subject = Semantics.namespace[f'{self._semantic_name}MeasurementResult'] + + sensor_triples = list(self._metadata.triples((None, Namespaces.sosa.isObservedBy, None))) + assert len(sensor_triples) == 1 + + # create observation node + data_graph.add((measurement_subject, Namespaces.rdf.type, rdflib.URIRef(Namespaces.sosa.Observation))) + data_graph.add((measurement_subject, Namespaces.schema.name, rdflib.Literal(f'{self._name} Measurement'))) + data_graph.add( + (measurement_subject, Namespaces.sosa.observedProperty, Semantics.namespace[self._semantic_name])) + data_graph.add((measurement_subject, Namespaces.sosa.hasResult, result_subject)) + data_graph.add((measurement_subject, Namespaces.sosa.madeBySensor, sensor_triples[0][2])) + data_graph.add((measurement_subject, Namespaces.schema.license, Semantics.data_license)) result = data_graph else: raise DeviceException('The provided kind of semantic information cannot be returned.') return result + def resolve_semantic_path(self, suffix: str) -> ('Element', ResourceType): + try: + return super().resolve_semantic_path(suffix) + except ChildNotFoundException: + if suffix == f'{self.semantic_name.split("/")[-1]}MeasurementResult': + return self, ResourceType.data + elif suffix == f'{self.semantic_name.split("/")[-1]}MeasurementUncertainty': + return self, ResourceType.uncertainty + elif suffix == f'{self.semantic_name.split("/")[-1]}Measurement': + return self, ResourceType.observation + + raise ChildNotFoundException('Could not resolve the semantic path.') + @property def semantic_name(self) -> str: if self._metadata is None: diff --git a/src/soil/semantics.py b/src/soil/semantics.py index 7a3f80159afb64a0937ab0064295ad9b6e0bc17d..9908084f3911b2d27ac9ab4e180379aaa4f5a431 100644 --- a/src/soil/semantics.py +++ b/src/soil/semantics.py @@ -37,6 +37,7 @@ class Semantics(object): class Namespaces(object): dcterms = rdflib.namespace.DCTERMS + earl = rdflib.Namespace('http://www.w3.org/ns/earl#') m4i = rdflib.Namespace('http://w3id.org/nfdi4ing/metadata4ing#') owl = rdflib.Namespace('http://www.w3.org/2002/07/owl#') quantitykind = rdflib.Namespace('http://qudt.org/vocab/quantitykind/') diff --git a/src/soil/stream.py b/src/soil/stream.py index ba6b4551159705bb0f43fd00c8629bb804932c7d..3b6ed19bef93abf8368ca507f91b91727d0706a3 100644 --- a/src/soil/stream.py +++ b/src/soil/stream.py @@ -1,16 +1,15 @@ import datetime import json from abc import ABC, abstractmethod -from typing import List, Callable, Any, Union, Dict +from typing import List, Callable, Any, Union, Dict, Tuple import rdflib from wzl.mqtt.client import MQTTPublisher from wzl.mqtt.exceptions import ClientNotFoundError +from . import variable from .component import Component from .event import Event -from . import variable -from .variable import Variable from .semantics import Namespaces from ..utils import root_logger from ..utils import serialize @@ -61,7 +60,12 @@ class Job(ABC): ... @property - def value(self) -> Any: + def value(self) -> Tuple[Any, Any]: + """ + + Returns: the value together with the covariance, which might None + + """ return self._callback() def is_triggered(self, time: datetime.datetime = None) -> bool: @@ -91,52 +95,45 @@ class Job(ABC): def stop(self) -> None: self._next = None - def _retrieve_metadata(self, model: Component = None): - if model is None: - return {} - try: - uuids = self.topic.split('/') - metadata = model.__getitem__(uuids).serialize([], False) - except Exception: - return {} - return metadata - - def _retrieve_semantic_metadata(self, model: Component = None) -> (str, rdflib.Graph): + def data(self, model: Component = None) -> Dict: if model is None: - return "", rdflib.Graph() + raise JobError() try: uuids = self.topic.split('/') - element = model.__getitem__(uuids) - return element.semantic_name, element.serialize_semantics(ResourceType.data) - except Exception: - return "", rdflib.Graph() + data = model.__getitem__(uuids).serialize([], False) - def data(self, model: Component = None) -> Dict: - try: - data = self._retrieve_metadata(model) + value, covariance = self.value data['uuid'] = self.topic - data['value'] = self.value + data['value'] = value + data['covariance'] = covariance data['timestamp'] = variable.serialize_time(datetime.datetime.now()) return data except Exception as e: raise JobError() def semantic_data(self, model: Component = None) -> (str, rdflib.Graph): + if model is None: + raise JobError() try: - url, data = self._retrieve_semantic_metadata(model) + uuids = self.topic.split('/') + element = model.__getitem__(uuids) + data = element.serialize_semantics(ResourceType.data) + data += element.serialize_semantics(ResourceType.uncertainty) + data += element.serialize_semantics(ResourceType.observation) + measurement_subject = \ list((data.subjects(predicate=Namespaces.rdf.type, object=Namespaces.soil.Measurement)))[0] # replace value data.remove((None, Namespaces.qudt.value, None)) - data.add((measurement_subject, Namespaces.qudt.value, self.serialize_value(data, self.value))) + value, covariance = self.value + data.add((measurement_subject, Namespaces.qudt.value, element.serialize_value(data, value))) # replace timestamp data.remove((None, Namespaces.schema.dateCreated, None)) data.add((measurement_subject, Namespaces.schema.dateCreated, rdflib.Literal(datetime.datetime.now()))) - # TODO add the uncertainty/covariance - return url, data + return element.semantic_name, data except Exception as e: raise JobError() diff --git a/src/soil/variable.py b/src/soil/variable.py index 1838d66eba612380667d84edbd2a8d27430c3358..5495dc393e47b631fbccf7145e9ccb9ff1d0e178 100644 --- a/src/soil/variable.py +++ b/src/soil/variable.py @@ -294,11 +294,11 @@ class Variable(Element, ABC): blank_node = rdflib.BNode() data_graph.add((blank_node, Namespaces.rdf.rest, Namespaces.rdf.nil)) data_graph.add( - (blank_node, Namespaces.rdf.first, Variable.serialize_value(data_graph, value[len(value) - 1]))) + (blank_node, Namespaces.rdf.first, self.serialize_value(data_graph, value[len(value) - 1]))) for entry in reversed(value[:-1]): new_blank_node = rdflib.BNode() data_graph.add((new_blank_node, Namespaces.rdf.rest, blank_node)) - data_graph.add((new_blank_node, Namespaces.rdf.first, Variable.serialize_value(data_graph, entry))) + data_graph.add((new_blank_node, Namespaces.rdf.first, self.serialize_value(data_graph, entry))) blank_node = new_blank_node return blank_node else: diff --git a/src/utils/resources.py b/src/utils/resources.py index 62857be528559991ba0f41cbca6ac06b8063db58..cc66e31cc054b9c8cdb678964b6c58f72ed7758f 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -4,10 +4,12 @@ from typing import List class ResourceType(enum.Enum): element = 0 - profile = 1 - metadata = 2 - data = 3 - range = 4 + profile = 1 # IRI -> <element name>Profile + metadata = 2 # IRI -> <element name> + data = 3 # IRI -> <measurement name>MeasurementResult + range = 4 # IRI -> <parameter/measurement/argument/return name>Range + observation = 5 # IRI -> <measurement name>Measurement + uncertainty = 6 # IRI -> <measurement name>MeasurementUncertainty def __str__(self) -> str: return self.name @@ -19,7 +21,8 @@ class ResourceType(enum.Enum): @classmethod @property def semantic_resources(cls) -> List[str]: - return [str(ResourceType.profile), str(ResourceType.metadata), str(ResourceType.data), str(ResourceType.range)] + return [str(ResourceType.profile), str(ResourceType.metadata), str(ResourceType.data), str(ResourceType.range), + str(ResourceType.observation), str(ResourceType.uncertainty)] def is_semantic(self) -> bool: - return self.value in range(1, 5) + return self.value in range(1, 7)