diff --git a/README.md b/README.md index ca76c2f68f97a23452397af837bc044b5f445433..efe36bfbf01b0c4675c0eebb7365a44de2f281ae 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,13 @@ Funded by the Deutsche Forschungsgemeinschaft (DFG, German Research Foundation) ## Recent changes +**9.2.0** - 2024-02-08 + - functions can be used to publish results via MQTT instead returning the results as response to the POST request + - if a function is implemented as generator this behaviour is triggered automatically + - bug fixes + - the semantic definition of the range of measurements and parameters are properly returned now + - profiles of base components of a component are properly returned now + **9.1.2** - 2024-01-19 - added "all" query parameter for semantics, to request complete semantic data model - fixed bug when requesting enum or time measurements and parameters diff --git a/src/http/server.py b/src/http/server.py index b8fb03079cce21802a696b02d3629b9a534d5696..35780cb623e682fe7263111fae73d91806fe97b8 100644 --- a/src/http/server.py +++ b/src/http/server.py @@ -226,15 +226,14 @@ class HTTPServer(object): if isinstance(item, Function): try: if item.publishes: - # generator = item.invoke_generator(data["arguments"]) try: - async for item in item.invoke_generator(data["arguments"]): + async for item in item.invoke_generator(data["arguments"], legacy_mode=self._legacy_mode): self._publisher.publish('/'.join(uuids), json.dumps(item)) response = {} except StopAsyncIteration: pass else: - response = await item.invoke(data["arguments"]) + response = await item.invoke(data["arguments"], legacy_mode=self._legacy_mode) status = 200 logger.info('Response: {}'.format(response)) except (DeviceException, ServerException, UserException) as e: diff --git a/src/soil/component.py b/src/soil/component.py index 8d2e26f6f7f5280ed2fe72d9233d6eca847ec88d..0ae5d4d023967b00bb6da72ca747d1e3bb51a18b 100644 --- a/src/soil/component.py +++ b/src/soil/component.py @@ -83,6 +83,7 @@ class Component(Element): self._components = components self._parameters = parameters self._implementation = implementation + self._profile_path: str = None @property def children(self) -> List[Element]: @@ -379,6 +380,7 @@ class Component(Element): def load_semantics(self, profiles_path: str, metadata_path: str, parent_name: str) -> None: super().load_semantics(profiles_path, metadata_path, parent_name) + self._profile_path = profiles_path for child in self.children: child.load_semantics(profiles_path, metadata_path, f"{parent_name}{self.uuid[4:].capitalize()}") @@ -392,7 +394,14 @@ class Component(Element): elif kind == 'metadata': result = self._metadata else: - raise DeviceException('The provided kind of semantic information cannot be returned.') + try: + shape_filename = os.path.join(self._profile_path, f'{kind.replace("Shape", "")}.shacl.ttl') + result = rdflib.Graph() + result.parse(shape_filename) + result.add((rdflib.URIRef(Semantics.namespace[kind]), Namespaces.dcterms.license, + Semantics.profile_license)) + except Exception: + raise DeviceException('The provided kind of semantic information cannot be returned.') if recursive: for child in self._components + self._measurements + self._parameters: @@ -400,7 +409,26 @@ class Component(Element): return result + def _is_semantic_path_of_base_profile(self, profile: rdflib.Graph, suffix: str) -> bool: + imported_profiles = list(profile.objects(predicate=Namespaces.owl.imports)) + for imported_profile in imported_profiles: + if Semantics.namespace not in imported_profile: + continue + imported_profile_name = imported_profile.toPython().replace(Semantics.namespace, '') + if imported_profile_name == suffix: + return True + else: + base_shape_filename = os.path.join(self._profile_path, f'{imported_profile_name.replace("Shape", "")}.shacl.ttl') + base_graph = rdflib.Graph().parse(base_shape_filename) + if self._is_semantic_path_of_base_profile(base_graph, suffix): + return True + return False + def resolve_semantic_path(self, suffix: str) -> (Element, str): + # we need to check FIRST if the requested semantic path refers to the profile of a base component of this component + if self._is_semantic_path_of_base_profile(self._metadata_profile, suffix): + return self, suffix + try: return super().resolve_semantic_path(suffix) except ChildNotFoundException: @@ -411,15 +439,6 @@ class Component(Element): except ChildNotFoundException: continue - # check if the profile of this component imports a shape which matches the path - imported_profiles = list(self._metadata_profile.objects(predicate=Namespaces.owl.imports)) - for imported_profile in imported_profiles: - if imported_profile.toPython().replace(Semantics.namespace, '') == suffix: - # TODO implement loading and returning of the base components profile - # (is not present as element, so might require adaption of the method signature, - # might also be called recursively, if the base component has own bases which are queried) - raise ChildNotFoundException('Profiles of base components can currently not be returned.') - raise ChildNotFoundException('Could not resolve the semantic path.') @property diff --git a/src/soil/figure.py b/src/soil/figure.py index 1b28f0788f105e779aca7288941be9b980b9897b..f833f03631f09ac7011a868b2bdb52f00ed95156 100644 --- a/src/soil/figure.py +++ b/src/soil/figure.py @@ -15,7 +15,7 @@ from .semantics import Namespaces nest_asyncio.apply() from .element import Element -from .error import DimensionException, RangeException, TypeException, NotImplementedException +from .error import DimensionException, RangeException, TypeException, NotImplementedException, ChildNotFoundException from ..utils import root_logger from ..utils.constants import HTTP_GET, HTTP_OPTIONS from ..utils.error import DeviceException @@ -99,6 +99,7 @@ class Figure(Element, ABC): predecessor=e) Figure.check_all(self._datatype, self._dimension, self._range, value) + self._value = value return value else: return self._value @@ -319,3 +320,13 @@ class Figure(Element, ABC): return self._getter else: raise NotImplementedException(self._uuid, self._name) + + def resolve_semantic_path(self, suffix: str) -> (Element, str): + try: + return super().resolve_semantic_path(suffix) + except ChildNotFoundException: + # check if the path fits the range + if suffix == f'{self.semantic_name.split("/")[-1]}Range': + return self, 'range' + + raise ChildNotFoundException('Could not resolve the semantic path.') diff --git a/src/soil/function.py b/src/soil/function.py index 504a5b381455a54869e5df55be2e83403633f357..1676f523f447e125acdfc23f72cb099982685bfe 100644 --- a/src/soil/function.py +++ b/src/soil/function.py @@ -1,3 +1,4 @@ +import datetime import inspect import json from typing import Any, Dict, List, Union, Callable @@ -6,7 +7,7 @@ import rdflib from .element import Element from .error import InvokationException, NotImplementedException, ChildNotFoundException -from .figure import Figure +from .figure import Figure, serialize_time from .parameter import Parameter from ..utils import root_logger from ..utils.constants import HTTP_GET, HTTP_OPTIONS @@ -87,7 +88,7 @@ class Function(Element): else: super().__setitem__(key, value) - def _prepare_invocation_result(self, result: Any) -> Dict[str, List[Dict[str, Any]]]: + def _prepare_invocation_result(self, result: Any, legacy_mode: bool = False) -> Dict[str, List[Dict[str, Any]]]: returns = {"returns": []} if result is not None: # if only one element is returned encapsulate result with tuple to make for-loop working @@ -107,10 +108,13 @@ class Function(Element): else: var = var[0] Figure.check_all(var.datatype, var.dimension, var.range, value) - returns['returns'] += [{'uuid': uuid, 'value': value}] + ret = self.__getitem__([uuid]).serialize([], legacy_mode, HTTP_OPTIONS) + ret['value'] = value + ret['timestamp'] = serialize_time(datetime.datetime.now()) + returns['returns'] += [ret] return returns - async def invoke_generator(self, arguments: List[Figure]) -> Dict[str, List[Dict[str, Any]]]: + async def invoke_generator(self, arguments: List[Figure], legacy_mode: bool = False) -> Dict[str, List[Dict[str, Any]]]: returns = {"returns": []} args = {} if self._implementation is None: @@ -127,7 +131,7 @@ class Function(Element): while True: try: result = await anext(generator) - yield self._prepare_invocation_result(result) + yield self._prepare_invocation_result(result, legacy_mode) except StopAsyncIteration as e: raise e else: @@ -136,13 +140,16 @@ class Function(Element): while True: try: result = next(generator) - yield self._prepare_invocation_result(result) + yield self._prepare_invocation_result(result, legacy_mode) except StopIteration as e: raise e + except StopIteration or StopAsyncIteration as e: + raise e except Exception as e: raise DeviceException(str(e), predecessor=e) - async def invoke(self, arguments: List[Figure]) -> Dict[str, List[Dict[str, Any]]]: + + async def invoke(self, arguments: List[Figure], legacy_mode: bool = False) -> Dict[str, List[Dict[str, Any]]]: args = {} if self._implementation is None: raise NotImplementedException(self._uuid, self._name) @@ -161,7 +168,7 @@ class Function(Element): except Exception as e: raise DeviceException(str(e), predecessor=e) - return self._prepare_invocation_result(result) + return self._prepare_invocation_result(result, legacy_mode) def serialize(self, keys: List[str], legacy_mode: bool, method: int = HTTP_GET) -> Dict[str, Any]: if not keys or 'all' in keys: diff --git a/src/soil/measurement.py b/src/soil/measurement.py index 35adbd3aa3fd7667f992d910956f4348c35f9739..6f30a7554ebbc9664ca8d7d55a4782570290fd2b 100644 --- a/src/soil/measurement.py +++ b/src/soil/measurement.py @@ -1,3 +1,4 @@ +import copy import datetime import warnings from typing import Dict, Callable, List @@ -177,6 +178,13 @@ class Measurement(Figure): result = self._metadata_profile elif kind == 'metadata': result = self._metadata + elif kind == 'range': + range_graph = copy.deepcopy(self._metadata) + subjects = range_graph.subjects() + for subject in subjects: + if subject != Semantics.namespace[f'{self._semantic_name}Range']: + range_graph.remove((subject, None, None)) + return range_graph elif kind == 'data': data_graph = rdflib.Graph() data_graph.bind('sosa', Namespaces.sosa) diff --git a/src/soil/parameter.py b/src/soil/parameter.py index 466d78882600c5c7370f1749976fc548f0bebc05..b70da8cad7c6b5e1633f7eaa81a9f63015a3e797 100644 --- a/src/soil/parameter.py +++ b/src/soil/parameter.py @@ -63,7 +63,7 @@ class Parameter(Figure): :return: the value of the attribute indicated by 'item'. """ if item == "constant": - return self._setter is None + return self._setter is None and self.uuid[:3] == 'PAR' return super().__getitem__(item, method) def serialize(self, keys: [str], legacy_mode: bool, method=HTTP_GET): @@ -139,6 +139,13 @@ class Parameter(Figure): def serialize_semantics(self, kind: str, recursive=False) -> rdflib.Graph: if kind == 'profile': result = self._metadata_profile + elif kind == 'range': + range_graph = copy.deepcopy(self._metadata) + subjects = range_graph.subjects() + for subject in subjects: + if subject != Semantics.namespace[f'{self._semantic_name}Range']: + range_graph.remove((subject, None, None)) + return range_graph elif kind == 'metadata': result = copy.deepcopy(self._metadata)