diff --git a/README.md b/README.md index 9e610619411f528ecbc92eba5100b60a52f17f07..67c8025e7eccb7a351181eca7b0c09a6016f4b0f 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.0 +Current stable version: 9.3.1 ## Installation 1. Install the WZL-UDI package via pip @@ -58,6 +58,9 @@ Funded by the Deutsche Forschungsgemeinschaft (DFG, German Research Foundation) ## Recent changes +**9.3.1** - 2024-03-14 + - fixed semantic name resolution + **9.3.0** - 2024-03-13 - implemented semantic features for functions - refactoring diff --git a/setup.py b/setup.py index 64733456bb6e0e707b5baf49626280898671ea75..85c2d036b5128dbdf7e2a9ec760f83208ddc783a 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.0', + version='9.3.1', 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 089f3590508d2d70c767653e5b2d8ba4ea82ff61..0b56d8e689c5077d0cbdec186b6b4d915e119599 100644 --- a/src/http/server.py +++ b/src/http/server.py @@ -2,6 +2,7 @@ import asyncio import functools import json +import os import traceback from typing import Dict, Union @@ -16,16 +17,17 @@ from .error import ServerException from ..soil.component import Component from ..soil.element import Element from ..soil.error import InvokationException, ReadOnlyException, ChildNotFoundException -from ..soil.variable import Variable from ..soil.function import Function from ..soil.measurement import Measurement from ..soil.parameter import Parameter -from ..soil.semantics import Semantics +from ..soil.semantics import Semantics, Namespaces from ..soil.stream import StreamScheduler +from ..soil.variable import Variable from ..utils import root_logger from ..utils import serialize from ..utils.constants import BASE_UUID_PATTERN, HTTP_GET, HTTP_OPTIONS -from ..utils.error import DeviceException, UserException +from ..utils.error import DeviceException, UserException, PathResolutionException +from ..utils.resources import ResourceType logger = root_logger.get(__name__) @@ -61,7 +63,8 @@ class HTTPServer(object): """ def __init__(self, loop: asyncio.AbstractEventLoop, host: str, port: int, model: Component, - dataformat: str = 'json', legacy_mode=False, scheduler: StreamScheduler = None, publisher: MQTTPublisher = None): + dataformat: str = 'json', legacy_mode=False, scheduler: StreamScheduler = None, + publisher: MQTTPublisher = None, profiles_path: str = None): """Constructor Args: @@ -84,6 +87,7 @@ class HTTPServer(object): self._legacy_mode = legacy_mode self._scheduler = scheduler self._publisher = publisher + self._profiles_path = profiles_path self.app = web.Application(loop=self.loop, middlewares=[cors]) @@ -103,6 +107,22 @@ class HTTPServer(object): web.run_app(self.app, host=self.host, port=self.port, loop=loop) logger.info('HTTP-Server serving on {}:{}'.format(host, port)) + @staticmethod + def analyze_request_url(request: Request) -> ResourceType: + assert request.url.parts[0] == '/' + + url_parts = request.url.parts + if url_parts[-1] == '': + url_parts = url_parts[:-1] + + if len(url_parts) == 3 and url_parts[1] == Semantics.prefix: + return ResourceType.profile if 'Profile' in url_parts[-1] else ResourceType.metadata + + if request.url.path == '/' or url_parts[-1][:3] in ['COM', 'FUN', 'PAR', 'MEA', 'ARG', 'RET']: + return ResourceType.element + + return ResourceType.metadata + @staticmethod def parse_uuids(request: Request): """Splits the request URL to extract the FQID of the targeted element of the SOIL-Interface. @@ -173,19 +193,40 @@ class HTTPServer(object): logger.info("GET Request from {}".format(request.url)) logger.debug('Request: {}'.format(request)) logger.debug('Query Parameters: {}'.format(request.query_string)) - splitted_request = HTTPServer.parse_uuids(request) + + resource_type = HTTPServer.analyze_request_url(request) keys = self._filter_query(request.query) - # determine whether a semantic answer is expected - semantic = None - if request.query is not None and 'semantic' in request.query and request.query['semantic'] in ['data', 'metadata', 'profile']: - semantic = request.query['semantic'] + if resource_type == ResourceType.profile: + if self._profiles_path is None: + raise UserException('Can\'t return requested metadata profile, as no profiles have been created for this sensor service.') - if len(splitted_request) > 0 and splitted_request[0] == Semantics.prefix: - item, semantic = self.root.resolve_semantic_path(splitted_request[1]) + profilename = request.url.parts[-2] if request.url.parts[-1] == '' else request.url.parts[-1] + + if len(profilename) > 12 and profilename[-12:-8] == 'Range': + filename = profilename.replace('RangeProfile', '.shacl.ttl') + else: + filename = profilename.replace('Profile', '.shacl.ttl') + + profile_path = os.path.join(self._profiles_path, filename) + response = rdflib.Graph() + response.parse(profile_path) + response.add( + (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: + item, resource_type = self.root.resolve_semantic_path(request.url.parts[-1]) else: + assert resource_type == ResourceType.element + uuids = HTTPServer.parse_uuids(request) + + if request.query is not None and 'semantic' in request.query and request.query[ + 'semantic'] in ResourceType.semantic_resources: + resource_type = ResourceType.from_string(request.query['semantic']) + try: - item = self.root[splitted_request] + item = self.root[uuids] except KeyError as e: logger.error(traceback.format_exc()) response = {'error': str(e)} @@ -194,10 +235,10 @@ class HTTPServer(object): # serializing the element try: - if semantic: + if resource_type.is_semantic() and resource_type != ResourceType.profile: recursive = request.query is not None and 'all' in request.query - response = item.serialize_semantics(semantic, recursive) - else: + response = item.serialize_semantics(resource_type, recursive) + elif resource_type == ResourceType.element: response = item.serialize(keys, self._legacy_mode, HTTP_GET) status = 200 logger.info('Response: {}'.format(response)) @@ -206,7 +247,8 @@ class HTTPServer(object): response = {'error': str(e)} status = 500 logger.error('Response: {}'.format(response)) - return self.prepare_response(response, item, status=status, query=request.query, semantic=semantic is not None) + + return self.prepare_response(response, item, status=status, query=request.query, semantic=resource_type.is_semantic()) async def post(self, request): logger.info("POST Request from {}".format(request.url)) diff --git a/src/soil/component.py b/src/soil/component.py index 1df9efc90cc869942c46b0ba811f95c85afea2de..7bafd96d1c3f9596c4c68012efa01a9bd54733fb 100644 --- a/src/soil/component.py +++ b/src/soil/component.py @@ -23,6 +23,7 @@ from .semantics import Namespaces, Semantics from ..utils import root_logger from ..utils.constants import HTTP_GET from ..utils.error import SerialisationException, DeviceException, UserException +from ..utils.resources import ResourceType logger = root_logger.get(__name__) @@ -385,27 +386,23 @@ class Component(Element): for child in self.children: child.load_semantics(profiles_path, metadata_path, f"{parent_name}{self.uuid[4:].capitalize()}") - def serialize_semantics(self, kind: str, recursive=False) -> rdflib.Graph: - if self._metadata_profile is None or self._metadata is None: - raise SerialisationException('No semantic information have been provided during initialization.') - - if kind == 'profile': + def serialize_semantics(self, resource_type: ResourceType, recursive=False) -> rdflib.Graph: + if resource_type == ResourceType.profile: + if self._metadata is None: + raise SerialisationException('No metadata profiles have been provided during initialization.') result = self._metadata_profile - elif kind == 'metadata': + + elif resource_type == ResourceType.metadata: + if self._metadata is None: + raise SerialisationException('No semantic information have been provided during initialization.') result = self._metadata + else: - try: - shape_filename = os.path.join(self._profile_path, f'{kind.replace("Profile", "")}.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.') + raise DeviceException('The provided kind of semantic information cannot be returned.') if recursive: for child in self.children: - result += child.serialize_semantics(kind, recursive) + result += child.serialize_semantics(resource_type, recursive) return result @@ -424,11 +421,7 @@ class Component(Element): 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 - + def resolve_semantic_path(self, suffix: str) -> (Element, ResourceType): try: return super().resolve_semantic_path(suffix) except ChildNotFoundException: diff --git a/src/soil/element.py b/src/soil/element.py index f3e8b95bc6ab7fce63065c258483376b21116c2c..0623b47d6dda4cdb081ed466884b7e4f8ad5b9be 100644 --- a/src/soil/element.py +++ b/src/soil/element.py @@ -9,6 +9,7 @@ from .error import ChildNotFoundException from .semantics import Namespaces, Semantics from ..utils.constants import BASE_UUID_PATTERN, HTTP_GET from ..utils.error import SerialisationException +from ..utils.resources import ResourceType class Element(ABC): @@ -113,14 +114,12 @@ class Element(ABC): self._metadata.add((rdflib.URIRef(self.semantic_name), Namespaces.schema.license, Semantics.metadata_license)) @abstractmethod - def serialize_semantics(self, kind: str, recursive: bool = False) -> rdflib.Graph: + def serialize_semantics(self, resource_type: ResourceType, recursive: bool = False) -> rdflib.Graph: ... - def resolve_semantic_path(self, suffix: str) -> ('Element', str): - if suffix == f'{self._profilename}Profile': - return self, 'profile' - elif suffix == self.semantic_name.split('/')[-1]: - return self, 'metadata' + def resolve_semantic_path(self, suffix: str) -> ('Element', ResourceType): + if suffix == self.semantic_name.split('/')[-1]: + return self, ResourceType.metadata raise ChildNotFoundException('Could not resolve the semantic path.') diff --git a/src/soil/function.py b/src/soil/function.py index aaafc792a86d194a1e80503e3acc190ab4d70c9c..fb4b4ec1ebf02c42354d78484f00109eba045f22 100644 --- a/src/soil/function.py +++ b/src/soil/function.py @@ -13,6 +13,7 @@ from .parameter import Parameter from ..utils import root_logger from ..utils.constants import HTTP_GET, HTTP_OPTIONS from ..utils.error import SerialisationException, DeviceException +from ..utils.resources import ResourceType logger = root_logger.get(__name__) @@ -234,24 +235,27 @@ class Function(Element): for child in self._arguments + self._returns: child.load_semantics(profiles_path, metadata_path, f"{parent_name}{self.uuid[4:].capitalize()}") - def serialize_semantics(self, kind: str, recursive=False) -> rdflib.Graph: - if self._metadata_profile is None or self._metadata is None: - raise SerialisationException('No semantic information have been provided during initialization.') - - if kind == 'profile': + def serialize_semantics(self, resource_type: ResourceType, recursive=False) -> rdflib.Graph: + if resource_type == ResourceType.profile: + if self._metadata is None: + raise SerialisationException('No metadata profiles have been provided during initialization.') result = self._metadata_profile - elif kind == 'metadata': + + elif resource_type == ResourceType.metadata: + if self._metadata is None: + raise SerialisationException('No semantic information have been provided during initialization.') + result = self._metadata else: raise DeviceException('The provided kind of semantic information cannot be returned.') if recursive: for child in self._arguments + self._returns: - result += child.serialize_semantics(kind, recursive) + result += child.serialize_semantics(resource_type, recursive) return result - def resolve_semantic_path(self, suffix: str) -> (Element, str): + def resolve_semantic_path(self, suffix: str) -> (Element, ResourceType): try: return super().resolve_semantic_path(suffix) except ChildNotFoundException: diff --git a/src/soil/measurement.py b/src/soil/measurement.py index f2b7a3f6d9e21518b1f7109a76b3fcc7cb4fc549..a2546e7d32de9c71244935a1eb30524a45d61d47 100644 --- a/src/soil/measurement.py +++ b/src/soil/measurement.py @@ -12,6 +12,7 @@ from .semantics import Semantics, Namespaces from ..utils import root_logger from ..utils.constants import HTTP_GET from ..utils.error import SerialisationException, DeviceException +from ..utils.resources import ResourceType logger = root_logger.get(__name__) @@ -173,19 +174,21 @@ class Measurement(Variable): except Exception as e: raise SerialisationException('{}: The measurement can not be deserialized. {}'.format(uuid, e)) - def serialize_semantics(self, kind: str, recursive=False) -> rdflib.Graph: - if kind == 'profile': + def serialize_semantics(self, resource_type: ResourceType, recursive=False) -> rdflib.Graph: + if resource_type == ResourceType.profile: + if self._metadata_profile is None: + raise SerialisationException('No metadata profiles have been provided during initialization.') result = self._metadata_profile - elif kind == 'metadata': + + elif resource_type == ResourceType.metadata: result = self._metadata - elif kind == 'range': - range_graph = copy.deepcopy(self._metadata) - subjects = range_graph.subjects() + elif resource_type == ResourceType.range: + result = copy.deepcopy(self._metadata) + subjects = result.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': + result.remove((subject, None, None)) + elif resource_type == ResourceType.data: data_graph = rdflib.Graph() data_graph.bind('sosa', Namespaces.sosa) data_graph.bind(Semantics.prefix, Semantics.namespace) diff --git a/src/soil/parameter.py b/src/soil/parameter.py index f7e862b6972d8c5856e157a34dbc5cc7639f9906..9743aa06a7600df7b23be53d2f599c5121700b31 100644 --- a/src/soil/parameter.py +++ b/src/soil/parameter.py @@ -7,11 +7,12 @@ import rdflib from .datatype import Datatype from .error import ReadOnlyException -from .variable import Variable from .semantics import Semantics, Namespaces +from .variable import Variable from ..utils import root_logger from ..utils.constants import HTTP_GET from ..utils.error import DeviceException, SerialisationException +from ..utils.resources import ResourceType logger = root_logger.get(__name__) @@ -136,17 +137,20 @@ class Parameter(Variable): else: raise ReadOnlyException(self._uuid, self._name) - def serialize_semantics(self, kind: str, recursive=False) -> rdflib.Graph: - if kind == 'profile': - result = self._metadata_profile - elif kind == 'range': + def serialize_semantics(self, resource_type: ResourceType, recursive=False) -> rdflib.Graph: + if resource_type == ResourceType.profile: + if self._metadata_profile is None: + raise SerialisationException('No metadata profiles have been provided during initialization.') + return self._metadata_profile + + elif resource_type == ResourceType.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': + elif resource_type == ResourceType.metadata: result = copy.deepcopy(self._metadata) triples = list(result.triples((None, Namespaces.qudt['value'], None))) @@ -159,7 +163,6 @@ class Parameter(Variable): return result else: raise DeviceException('The provided kind of semantic information cannot be returned.') - return result @property def semantic_name(self) -> str: diff --git a/src/soil/variable.py b/src/soil/variable.py index 3f977369a31668263c3dbb0be27a525c534294ba..c0e59349db581982820e0f12915d1407c5baab6f 100644 --- a/src/soil/variable.py +++ b/src/soil/variable.py @@ -11,6 +11,7 @@ import strict_rfc3339 as rfc3339 from .datatype import Datatype from .semantics import Namespaces +from ..utils.resources import ResourceType nest_asyncio.apply() @@ -321,12 +322,12 @@ class Variable(Element, ABC): else: raise NotImplementedException(self._uuid, self._name) - def resolve_semantic_path(self, suffix: str) -> (Element, str): + def resolve_semantic_path(self, suffix: str) -> (Element, ResourceType): 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' + return self, ResourceType.range raise ChildNotFoundException('Could not resolve the semantic path.') diff --git a/src/utils/error.py b/src/utils/error.py index a6ccecfcec16e9e6e8a943a416506d0234ba2262..b1cbf3044e56ecd6e96e85bb80d9193c07d557d7 100644 --- a/src/utils/error.py +++ b/src/utils/error.py @@ -31,4 +31,10 @@ class UserException(BasicException): class SerialisationException(DeviceException): def __init__(self, description): - BasicException.__init__(self, description) \ No newline at end of file + BasicException.__init__(self, description) + + +class PathResolutionException(BasicException): + + def __init__(self, description, stack_trace=None, predecessor=None): + BasicException.__init__(self, description, stack_trace, predecessor) diff --git a/src/utils/resources.py b/src/utils/resources.py new file mode 100644 index 0000000000000000000000000000000000000000..62857be528559991ba0f41cbca6ac06b8063db58 --- /dev/null +++ b/src/utils/resources.py @@ -0,0 +1,25 @@ +import enum +from typing import List + + +class ResourceType(enum.Enum): + element = 0 + profile = 1 + metadata = 2 + data = 3 + range = 4 + + def __str__(self) -> str: + return self.name + + @classmethod + def from_string(cls, value: str) -> 'ResourceType': + return cls.__members__[value] + + @classmethod + @property + def semantic_resources(cls) -> List[str]: + return [str(ResourceType.profile), str(ResourceType.metadata), str(ResourceType.data), str(ResourceType.range)] + + def is_semantic(self) -> bool: + return self.value in range(1, 5)