Skip to content
Snippets Groups Projects
Commit 49f634c6 authored by Matthias Stefan Bodenbenner's avatar Matthias Stefan Bodenbenner
Browse files

implemented semantic path resolution for ranges and base component profiles

parent de701715
No related branches found
No related tags found
No related merge requests found
Pipeline #356751 passed
......@@ -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
......
......@@ -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:
......
......@@ -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,6 +394,13 @@ class Component(Element):
elif kind == 'metadata':
result = self._metadata
else:
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:
......@@ -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
......
......@@ -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.')
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:
......
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)
......
......@@ -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)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment