From d7b9e438c0a7674d9defd41454d99d3fbc849150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 5 Jun 2020 15:39:25 +0200 Subject: [PATCH 001/157] adapter.http: add first working draft --- basyx/aas/adapter/http.py | 142 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + setup.py | 1 + 3 files changed, 144 insertions(+) create mode 100644 basyx/aas/adapter/http.py diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py new file mode 100644 index 000000000..e72240954 --- /dev/null +++ b/basyx/aas/adapter/http.py @@ -0,0 +1,142 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import base64 +from binascii import Error as Base64Error +from werkzeug.datastructures import MIMEAccept, Headers +from werkzeug.exceptions import BadRequest, HTTPException, NotAcceptable, NotImplemented +from werkzeug.http import parse_accept_header +from werkzeug.routing import Map, Rule, Submount +from werkzeug.wrappers import Request, Response + +from .. import model +from ._generic import IDENTIFIER_TYPES_INVERSE +from .json.json_serialization import asset_administration_shell_to_json + +from typing import Dict, Iterable, Optional, Type + + +class JsonResponse(Response): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, content_type="application/json") + + +class XmlResponse(Response): + def __init__(self, *args, content_type="application/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + +class XmlResponseAlt(XmlResponse): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, content_type="text/xml") + + + """ + A mapping of supported content types to their respective ResponseType. + The first content type in this dict will be preferred if the requester doesn't specify preferred content types using + and HTTP-"Accept" header. + """ +RESPONSE_TYPES = { + "application/json": JsonResponse, +# "application/xml": XmlResponse, +# "text/xml": XmlResponseAlt +} + + +class WSGIApp: + def __init__(self, object_store: model.AbstractObjectStore): + self.object_store: model.AbstractObjectStore = object_store + self.url_map: Map = Map([ + Submount("/api/v1", [ + # TODO: custom decoder for base64 + Rule("/shells//aas", methods=["GET"], endpoint=self.get_aas), + Rule("/shells//abc") # no endpoint => 501 not implemented + ]) + ]) + + def __call__(self, environ, start_response): + response = self.handle_request(Request(environ)) + return response(environ, start_response) + + @classmethod + def preferred_content_type(cls, headers: Headers, content_types: Iterable[str]) -> Optional[str]: + accept_str: Optional[str] = headers.get("accept") + if accept_str is None: + # return first content type in case accept http header is not specified + return next(iter(content_types)) + accept: MIMEAccept = parse_accept_header(accept_str, MIMEAccept) + return accept.best_match(content_types) + + @classmethod + def base64_param(cls, args: Dict[str, str], param: str) -> str: + try: + b64decoded = base64.b64decode(args[param]) + except Base64Error: + raise BadRequest(f"URL-Parameter '{param}' with value '{args[param]}' is not a valid base64 string!") + try: + return b64decoded.decode("utf-8") + except UnicodeDecodeError: + raise BadRequest(f"URL-Parameter '{param}' with base64 decoded value '{b64decoded!r}' is not valid utf-8!") + + @classmethod + def identifier_from_param(cls, args: Dict[str, str], param: str) -> model.Identifier: + id_type, id_ = cls.base64_param(args, param).split(":", 1) + try: + return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type]) + except KeyError: + raise BadRequest(f"'{id_type}' is not a valid identifier type!") + + # this is not used yet + @classmethod + def mandatory_request_param(cls, request: Request, param: str) -> str: + try: + return request.args[param] + except KeyError: + raise BadRequest(f"Parameter '{param}' is mandatory") + + def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: + identifiable = self.object_store.get(identifier) + if not isinstance(identifiable, type_): + raise BadRequest(f"Object specified by id {identifier} is of unexpected type {type(identifiable)}! " + f"Expected type: {type_}") + return identifiable + + def handle_request(self, request: Request): + adapter = self.url_map.bind_to_environ(request.environ) + # determine response content type + # TODO: implement xml responses + content_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) + if content_type is None: + return NotAcceptable(f"This server supports the following content types: " + + ", ".join(RESPONSE_TYPES.keys())) + response_type = RESPONSE_TYPES[content_type] + try: + endpoint, values = adapter.match() + if endpoint is None: + return NotImplemented("This route is not yet implemented.") + endpoint(request, values, response_type) + except HTTPException as e: + # raised error leaving this function => 500 + return e + + # http api issues (depth parameter, repository interface (id encoding)) + def get_aas(self, request: Request, args: Dict[str, str], response_type: Type[Response]): + # TODO: depth parameter + aas_id = self.identifier_from_param(args, "aas_id") + aas = self.get_obj_ts(aas_id, model.AssetAdministrationShell) + # TODO: encode with xml for xml responses + return response_type(asset_administration_shell_to_json(aas)) + + +if __name__ == "__main__": + from werkzeug.serving import run_simple + from aas.examples.data.example_aas import create_full_example + run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) diff --git a/requirements.txt b/requirements.txt index 41079b33c..fe3d07b24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ python-dateutil>=2.8,<3.0 types-python-dateutil pyecma376-2>=0.2.4 urllib3>=1.26,<2.0 +Werkzeug>=1.0.1,<1.1 diff --git a/setup.py b/setup.py index bc5fe4b65..ff5f24b78 100755 --- a/setup.py +++ b/setup.py @@ -46,5 +46,6 @@ 'lxml>=4.2,<5', 'urllib3>=1.26,<2.0', 'pyecma376-2>=0.2.4', + 'Werkzeug>=1.0.1,<1.1' ] ) From 2b9c0610216689a557a3743cd322e647c6e181d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 7 Jun 2020 20:15:50 +0200 Subject: [PATCH 002/157] adapter.http: fix codestyle --- basyx/aas/adapter/http.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e72240954..3cd43292c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -39,15 +39,15 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs, content_type="text/xml") - """ - A mapping of supported content types to their respective ResponseType. - The first content type in this dict will be preferred if the requester doesn't specify preferred content types using - and HTTP-"Accept" header. - """ +""" +A mapping of supported content types to their respective ResponseType. +The first content type in this dict will be preferred if the requester doesn't specify preferred content types using +and HTTP-"Accept" header. +""" RESPONSE_TYPES = { "application/json": JsonResponse, -# "application/xml": XmlResponse, -# "text/xml": XmlResponseAlt + # "application/xml": XmlResponse, + # "text/xml": XmlResponseAlt } From a8d4ded0bd3e9e91e302fb2474ab6c2a1d8ad055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 14 Jun 2020 13:56:08 +0200 Subject: [PATCH 003/157] change maximum werkzeug version to <2 list werkzeug as a dependency in the readme --- README.md | 1 + requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 63b5de26b..04f18a92d 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ The BaSyx Python SDK requires the following Python packages to be installed for * `python-dateutil` (BSD 3-clause License) * `pyecma376-2` (Apache License v2.0) * `urllib3` (MIT License) +* `Werkzeug` (BSD 3-clause License) Optional production usage dependencies: * For using the Compliance Tool to validate JSON files against the JSON Schema: `jsonschema` and its diff --git a/requirements.txt b/requirements.txt index fe3d07b24..f023f178d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ python-dateutil>=2.8,<3.0 types-python-dateutil pyecma376-2>=0.2.4 urllib3>=1.26,<2.0 -Werkzeug>=1.0.1,<1.1 +Werkzeug>=1.0.1,<2 diff --git a/setup.py b/setup.py index ff5f24b78..e50c50624 100755 --- a/setup.py +++ b/setup.py @@ -46,6 +46,6 @@ 'lxml>=4.2,<5', 'urllib3>=1.26,<2.0', 'pyecma376-2>=0.2.4', - 'Werkzeug>=1.0.1,<1.1' + 'Werkzeug>=1.0.1,<2' ] ) From f56149f9048c426cd038d82ed3b9e0eed16d2db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 14 Jun 2020 14:12:17 +0200 Subject: [PATCH 004/157] adapter.http: serialize response data in xml/json response classes adapter.http: add custom identifier converter for werkzeug adapter.http: restructure routing map, add first submodel route adapter.http: refine imports --- basyx/aas/adapter/http.py | 143 ++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 54 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 3cd43292c..211f42780 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -9,91 +9,123 @@ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. -import base64 -from binascii import Error as Base64Error -from werkzeug.datastructures import MIMEAccept, Headers -from werkzeug.exceptions import BadRequest, HTTPException, NotAcceptable, NotImplemented -from werkzeug.http import parse_accept_header -from werkzeug.routing import Map, Rule, Submount +import json +from lxml import etree # type: ignore +import werkzeug +from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented +from werkzeug.routing import Rule, Submount from werkzeug.wrappers import Request, Response from .. import model from ._generic import IDENTIFIER_TYPES_INVERSE -from .json.json_serialization import asset_administration_shell_to_json +from .json import json_serialization +from .xml import xml_serialization from typing import Dict, Iterable, Optional, Type -class JsonResponse(Response): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs, content_type="application/json") +def xml_element_to_str(element: etree.Element) -> str: + # namespaces will just get assigned a prefix like nsX, where X is a positive integer + # "aas" would be a better prefix for the AAS namespace + # TODO: find a way to specify a namespace map when serializing + return etree.tostring(element, xml_declaration=True, encoding="utf-8") + + +class APIResponse(Response): + def __init__(self, data, *args, **kwargs): + super().__init__(*args, **kwargs) + if isinstance(data, model.AssetAdministrationShell): + self.data = self.serialize_aas(data) + elif isinstance(data, model.Submodel): + self.data = self.serialize_sm(data) + # TODO: encode non-data responses with json/xml as well (e.g. results and errors) + + def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: + pass + + def serialize_sm(self, aas: model.Submodel) -> str: + pass + + +class JsonResponse(APIResponse): + def __init__(self, *args, content_type="application/json", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: + return json.dumps(aas, cls=json_serialization.AASToJsonEncoder) + def serialize_sm(self, sm: model.Submodel) -> str: + return json.dumps(sm, cls=json_serialization.AASToJsonEncoder) -class XmlResponse(Response): + +class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) + def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: + return xml_element_to_str(xml_serialization.asset_administration_shell_to_xml(aas)) + + def serialize_sm(self, sm: model.Submodel) -> str: + return xml_element_to_str(xml_serialization.submodel_to_xml(sm)) + class XmlResponseAlt(XmlResponse): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs, content_type="text/xml") + def __init__(self, *args, content_type="text/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) """ A mapping of supported content types to their respective ResponseType. -The first content type in this dict will be preferred if the requester doesn't specify preferred content types using -and HTTP-"Accept" header. +The first content type in this dict will be preferred if the requester +doesn't specify preferred content types using the HTTP Accept header. """ RESPONSE_TYPES = { "application/json": JsonResponse, - # "application/xml": XmlResponse, - # "text/xml": XmlResponseAlt + "application/xml": XmlResponse, + "text/xml": XmlResponseAlt } +class IdentifierConverter(werkzeug.routing.PathConverter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def to_python(self, value) -> model.Identifier: + id_type, id_ = super().to_python(value).split(":", 1) + try: + return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type]) + except KeyError: + raise BadRequest(f"'{id_type}' is not a valid identifier type!") + + class WSGIApp: def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store - self.url_map: Map = Map([ + self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - # TODO: custom decoder for base64 - Rule("/shells//aas", methods=["GET"], endpoint=self.get_aas), - Rule("/shells//abc") # no endpoint => 501 not implemented + Submount("/shells/", [ + Rule("/aas", methods=["GET"], endpoint=self.get_aas) + ]), + Submount("/submodels/", [ + Rule("/submodel", methods=["GET"], endpoint=self.get_sm) + ]) ]) - ]) + ], converters={"identifier": IdentifierConverter}) def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) return response(environ, start_response) @classmethod - def preferred_content_type(cls, headers: Headers, content_types: Iterable[str]) -> Optional[str]: + def preferred_content_type(cls, headers: werkzeug.datastructures.Headers, content_types: Iterable[str]) \ + -> Optional[str]: accept_str: Optional[str] = headers.get("accept") if accept_str is None: # return first content type in case accept http header is not specified return next(iter(content_types)) - accept: MIMEAccept = parse_accept_header(accept_str, MIMEAccept) + accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) return accept.best_match(content_types) - @classmethod - def base64_param(cls, args: Dict[str, str], param: str) -> str: - try: - b64decoded = base64.b64decode(args[param]) - except Base64Error: - raise BadRequest(f"URL-Parameter '{param}' with value '{args[param]}' is not a valid base64 string!") - try: - return b64decoded.decode("utf-8") - except UnicodeDecodeError: - raise BadRequest(f"URL-Parameter '{param}' with base64 decoded value '{b64decoded!r}' is not valid utf-8!") - - @classmethod - def identifier_from_param(cls, args: Dict[str, str], param: str) -> model.Identifier: - id_type, id_ = cls.base64_param(args, param).split(":", 1) - try: - return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type]) - except KeyError: - raise BadRequest(f"'{id_type}' is not a valid identifier type!") - # this is not used yet @classmethod def mandatory_request_param(cls, request: Request, param: str) -> str: @@ -105,14 +137,12 @@ def mandatory_request_param(cls, request: Request, param: str) -> str: def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): - raise BadRequest(f"Object specified by id {identifier} is of unexpected type {type(identifiable)}! " - f"Expected type: {type_}") + raise NotFound(f"No '{type_.__name__}' with id '{identifier}' found!") return identifiable def handle_request(self, request: Request): adapter = self.url_map.bind_to_environ(request.environ) # determine response content type - # TODO: implement xml responses content_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) if content_type is None: return NotAcceptable(f"This server supports the following content types: " @@ -122,18 +152,23 @@ def handle_request(self, request: Request): endpoint, values = adapter.match() if endpoint is None: return NotImplemented("This route is not yet implemented.") - endpoint(request, values, response_type) - except HTTPException as e: - # raised error leaving this function => 500 + return endpoint(request, values, response_type) + # any raised error that leaves this function will cause a 500 internal server error + # so catch raised http exceptions and return them + # TODO: apply response types to http exceptions + except werkzeug.exceptions.HTTPException as e: return e # http api issues (depth parameter, repository interface (id encoding)) - def get_aas(self, request: Request, args: Dict[str, str], response_type: Type[Response]): + def get_aas(self, request: Request, args: Dict, response_type: Type[APIResponse]) -> APIResponse: + # TODO: depth parameter + aas = self.get_obj_ts(args["aas_id"], model.AssetAdministrationShell) + return response_type(aas) + + def get_sm(self, request: Request, args: Dict, response_type: Type[APIResponse]) -> APIResponse: # TODO: depth parameter - aas_id = self.identifier_from_param(args, "aas_id") - aas = self.get_obj_ts(aas_id, model.AssetAdministrationShell) - # TODO: encode with xml for xml responses - return response_type(asset_administration_shell_to_json(aas)) + sm = self.get_obj_ts(args["sm_id"], model.Submodel) + return response_type(sm) if __name__ == "__main__": From 45c83df10e926ac20e34c287b3b2c35eb0a95c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 2 Jul 2020 13:35:30 +0200 Subject: [PATCH 005/157] adapter.http: move to folder with seperate files adapter._generic: add identifier URI encode/decode functions adapter.http: make api routes return ResponseData instead of the Response directly adapter.http: add Result + Message types + factory function --- basyx/aas/adapter/_generic.py | 16 +++ basyx/aas/adapter/http.py | 177 ----------------------------- basyx/aas/adapter/http/__init__.py | 12 ++ basyx/aas/adapter/http/__main__.py | 17 +++ basyx/aas/adapter/http/response.py | 115 +++++++++++++++++++ basyx/aas/adapter/http/wsgi.py | 110 ++++++++++++++++++ 6 files changed, 270 insertions(+), 177 deletions(-) delete mode 100644 basyx/aas/adapter/http.py create mode 100644 basyx/aas/adapter/http/__init__.py create mode 100644 basyx/aas/adapter/http/__main__.py create mode 100644 basyx/aas/adapter/http/response.py create mode 100644 basyx/aas/adapter/http/wsgi.py diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index 34c3412b1..a65cf54e5 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -11,6 +11,7 @@ from typing import Dict, Type from basyx.aas import model +import urllib.parse # XML Namespace definition XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} @@ -113,3 +114,18 @@ KEY_TYPES_CLASSES_INVERSE: Dict[model.KeyTypes, Type[model.Referable]] = \ {v: k for k, v in model.KEY_TYPES_CLASSES.items()} + + +def identifier_uri_encode(id_: model.Identifier) -> str: + return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") + + +def identifier_uri_decode(id_str: str) -> model.Identifier: + try: + id_type_str, id_ = id_str.split(":", 1) + except ValueError as e: + raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") + id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) + if id_type is None: + raise ValueError(f"Identifier Type '{id_type_str}' is invalid") + return model.Identifier(urllib.parse.unquote(id_), id_type) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py deleted file mode 100644 index 211f42780..000000000 --- a/basyx/aas/adapter/http.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -import json -from lxml import etree # type: ignore -import werkzeug -from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented -from werkzeug.routing import Rule, Submount -from werkzeug.wrappers import Request, Response - -from .. import model -from ._generic import IDENTIFIER_TYPES_INVERSE -from .json import json_serialization -from .xml import xml_serialization - -from typing import Dict, Iterable, Optional, Type - - -def xml_element_to_str(element: etree.Element) -> str: - # namespaces will just get assigned a prefix like nsX, where X is a positive integer - # "aas" would be a better prefix for the AAS namespace - # TODO: find a way to specify a namespace map when serializing - return etree.tostring(element, xml_declaration=True, encoding="utf-8") - - -class APIResponse(Response): - def __init__(self, data, *args, **kwargs): - super().__init__(*args, **kwargs) - if isinstance(data, model.AssetAdministrationShell): - self.data = self.serialize_aas(data) - elif isinstance(data, model.Submodel): - self.data = self.serialize_sm(data) - # TODO: encode non-data responses with json/xml as well (e.g. results and errors) - - def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: - pass - - def serialize_sm(self, aas: model.Submodel) -> str: - pass - - -class JsonResponse(APIResponse): - def __init__(self, *args, content_type="application/json", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: - return json.dumps(aas, cls=json_serialization.AASToJsonEncoder) - - def serialize_sm(self, sm: model.Submodel) -> str: - return json.dumps(sm, cls=json_serialization.AASToJsonEncoder) - - -class XmlResponse(APIResponse): - def __init__(self, *args, content_type="application/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - def serialize_aas(self, aas: model.AssetAdministrationShell) -> str: - return xml_element_to_str(xml_serialization.asset_administration_shell_to_xml(aas)) - - def serialize_sm(self, sm: model.Submodel) -> str: - return xml_element_to_str(xml_serialization.submodel_to_xml(sm)) - - -class XmlResponseAlt(XmlResponse): - def __init__(self, *args, content_type="text/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - -""" -A mapping of supported content types to their respective ResponseType. -The first content type in this dict will be preferred if the requester -doesn't specify preferred content types using the HTTP Accept header. -""" -RESPONSE_TYPES = { - "application/json": JsonResponse, - "application/xml": XmlResponse, - "text/xml": XmlResponseAlt -} - - -class IdentifierConverter(werkzeug.routing.PathConverter): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def to_python(self, value) -> model.Identifier: - id_type, id_ = super().to_python(value).split(":", 1) - try: - return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type]) - except KeyError: - raise BadRequest(f"'{id_type}' is not a valid identifier type!") - - -class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore): - self.object_store: model.AbstractObjectStore = object_store - self.url_map = werkzeug.routing.Map([ - Submount("/api/v1", [ - Submount("/shells/", [ - Rule("/aas", methods=["GET"], endpoint=self.get_aas) - ]), - Submount("/submodels/", [ - Rule("/submodel", methods=["GET"], endpoint=self.get_sm) - ]) - ]) - ], converters={"identifier": IdentifierConverter}) - - def __call__(self, environ, start_response): - response = self.handle_request(Request(environ)) - return response(environ, start_response) - - @classmethod - def preferred_content_type(cls, headers: werkzeug.datastructures.Headers, content_types: Iterable[str]) \ - -> Optional[str]: - accept_str: Optional[str] = headers.get("accept") - if accept_str is None: - # return first content type in case accept http header is not specified - return next(iter(content_types)) - accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) - return accept.best_match(content_types) - - # this is not used yet - @classmethod - def mandatory_request_param(cls, request: Request, param: str) -> str: - try: - return request.args[param] - except KeyError: - raise BadRequest(f"Parameter '{param}' is mandatory") - - def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: - identifiable = self.object_store.get(identifier) - if not isinstance(identifiable, type_): - raise NotFound(f"No '{type_.__name__}' with id '{identifier}' found!") - return identifiable - - def handle_request(self, request: Request): - adapter = self.url_map.bind_to_environ(request.environ) - # determine response content type - content_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) - if content_type is None: - return NotAcceptable(f"This server supports the following content types: " - + ", ".join(RESPONSE_TYPES.keys())) - response_type = RESPONSE_TYPES[content_type] - try: - endpoint, values = adapter.match() - if endpoint is None: - return NotImplemented("This route is not yet implemented.") - return endpoint(request, values, response_type) - # any raised error that leaves this function will cause a 500 internal server error - # so catch raised http exceptions and return them - # TODO: apply response types to http exceptions - except werkzeug.exceptions.HTTPException as e: - return e - - # http api issues (depth parameter, repository interface (id encoding)) - def get_aas(self, request: Request, args: Dict, response_type: Type[APIResponse]) -> APIResponse: - # TODO: depth parameter - aas = self.get_obj_ts(args["aas_id"], model.AssetAdministrationShell) - return response_type(aas) - - def get_sm(self, request: Request, args: Dict, response_type: Type[APIResponse]) -> APIResponse: - # TODO: depth parameter - sm = self.get_obj_ts(args["sm_id"], model.Submodel) - return response_type(sm) - - -if __name__ == "__main__": - from werkzeug.serving import run_simple - from aas.examples.data.example_aas import create_full_example - run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) diff --git a/basyx/aas/adapter/http/__init__.py b/basyx/aas/adapter/http/__init__.py new file mode 100644 index 000000000..27808a859 --- /dev/null +++ b/basyx/aas/adapter/http/__init__.py @@ -0,0 +1,12 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from .wsgi import WSGIApp diff --git a/basyx/aas/adapter/http/__main__.py b/basyx/aas/adapter/http/__main__.py new file mode 100644 index 000000000..59bf98bb3 --- /dev/null +++ b/basyx/aas/adapter/http/__main__.py @@ -0,0 +1,17 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from werkzeug.serving import run_simple + +from aas.examples.data.example_aas import create_full_example +from . import WSGIApp + +run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py new file mode 100644 index 000000000..196976d06 --- /dev/null +++ b/basyx/aas/adapter/http/response.py @@ -0,0 +1,115 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import abc +import enum +import json +from lxml import etree # type: ignore +import werkzeug.exceptions +from werkzeug.wrappers import Response + +from aas import model +from aas.adapter.json import json_serialization +from aas.adapter.xml import xml_serialization + +from typing import Dict, List, Optional, Type, Union + + +@enum.unique +class MessageType(enum.Enum): + UNSPECIFIED = enum.auto() + DEBUG = enum.auto() + INFORMATION = enum.auto() + WARNING = enum.auto() + ERROR = enum.auto() + FATAL = enum.auto() + EXCEPTION = enum.auto() + + +class Message: + def __init__(self, code: str, text: str, message_type: MessageType = MessageType.UNSPECIFIED): + self.code = code + self.text = text + self.message_type = message_type + + +class Result: + def __init__(self, success: bool, is_exception: bool, messages: List[Message]): + self.success = success + self.is_exception = is_exception + self.messages = messages + + +ResponseDataType = Union[Result, model.Referable, List[model.Referable]] + + +class ResponseData(BaseException): + def __init__(self, data: ResponseDataType, http_status_code: int = 200): + self.data = data + self.http_status_code = 200 + + +class APIResponse(abc.ABC, Response): + def __init__(self, data: ResponseData, *args, **kwargs): + super().__init__(*args, **kwargs) + self.status_code = data.http_status_code + self.data = self.serialize(data.data) + + @abc.abstractmethod + def serialize(self, data: ResponseDataType) -> str: + pass + + +class JsonResponse(APIResponse): + def __init__(self, *args, content_type="application/json", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize(self, data: ResponseDataType) -> str: + return json.dumps(data, cls=json_serialization.AASToJsonEncoder) + + +class XmlResponse(APIResponse): + def __init__(self, *args, content_type="application/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize(self, data: ResponseDataType) -> str: + return "" + + +class XmlResponseAlt(XmlResponse): + def __init__(self, *args, content_type="text/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + +""" +A mapping of supported content types to their respective ResponseType. +The first content type in this dict will be preferred if the requester +doesn't specify preferred content types using the HTTP Accept header. +""" +RESPONSE_TYPES: Dict[str, Type[APIResponse]] = { + "application/json": JsonResponse, + "application/xml": XmlResponse, + "text/xml": XmlResponseAlt +} + + +def create_result_response(code: str, text: str, message_type: MessageType, http_status_code: int = 200, + success: bool = False, is_exception: bool = False) -> ResponseData: + message = Message(code, text, message_type) + result = Result(success, is_exception, [message]) + return ResponseData(result, http_status_code) + + +def xml_element_to_str(element: etree.Element) -> str: + # namespaces will just get assigned a prefix like nsX, where X is a positive integer + # "aas" would be a better prefix for the AAS namespace + # TODO: find a way to specify a namespace map when serializing + return etree.tostring(element, xml_declaration=True, encoding="utf-8") diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py new file mode 100644 index 000000000..6762169ea --- /dev/null +++ b/basyx/aas/adapter/http/wsgi.py @@ -0,0 +1,110 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + + +import werkzeug +from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented +from werkzeug.routing import Rule, Submount +from werkzeug.wrappers import Request + +from aas import model + +from .response import RESPONSE_TYPES, MessageType, ResponseData, create_result_response +from .._generic import identifier_uri_decode, identifier_uri_encode + +from typing import Dict, Iterable, Optional, Type + + +class IdentifierConverter(werkzeug.routing.UnicodeConverter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def to_url(self, value: model.Identifier) -> str: + return super().to_url(identifier_uri_encode(value)) + + def to_python(self, value: str) -> model.Identifier: + try: + return identifier_uri_decode(super().to_python(value)) + except ValueError as e: + raise BadRequest(str(e)) + + +class WSGIApp: + def __init__(self, object_store: model.AbstractObjectStore): + self.object_store: model.AbstractObjectStore = object_store + self.url_map = werkzeug.routing.Map([ + Submount("/api/v1.0", [ + Submount("/shells/", [ + Rule("/aas", methods=["GET"], endpoint=self.get_aas) + ]), + Submount("/submodels/", [ + Rule("/submodel", methods=["GET"], endpoint=self.get_sm) + ]) + ]) + ], converters={"identifier": IdentifierConverter}) + + def __call__(self, environ, start_response): + response = self.handle_request(Request(environ)) + return response(environ, start_response) + + @classmethod + def preferred_content_type(cls, headers: werkzeug.datastructures.Headers, content_types: Iterable[str]) \ + -> Optional[str]: + accept_str: Optional[str] = headers.get("accept") + if accept_str is None: + # return first content type in case accept http header is not specified + return next(iter(content_types)) + accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) + return accept.best_match(content_types) + + # this is not used yet + @classmethod + def mandatory_request_param(cls, request: Request, param: str) -> str: + req_param = request.args.get(param) + if req_param is None: + raise create_result_response("mandatory_param_missing", f"Parameter '{param}' is mandatory", + MessageType.ERROR, 400) + return req_param + + def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: + identifiable = self.object_store.get(identifier) + if not isinstance(identifiable, type_): + raise create_result_response("identifier_not_found", f"No {type_.__name__} with {identifier} found!", + MessageType.ERROR, 404) + return identifiable + + def handle_request(self, request: Request): + adapter = self.url_map.bind_to_environ(request.environ) + # determine response content type + preferred_response_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) + if preferred_response_type is None: + return NotAcceptable(f"This server supports the following content types: " + + ", ".join(RESPONSE_TYPES.keys())) + response_type = RESPONSE_TYPES[preferred_response_type] + try: + endpoint, values = adapter.match() + if endpoint is None: + return NotImplemented("This route is not yet implemented.") + return response_type(endpoint(request, values)) + # any raised error that leaves this function will cause a 500 internal server error + # so catch raised http exceptions and return them + except werkzeug.exceptions.HTTPException as e: + return e + except ResponseData as rd: + return response_type(rd) + + def get_aas(self, request: Request, url_args: Dict) -> ResponseData: + # TODO: depth parameter + return ResponseData(self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + + def get_sm(self, request: Request, url_args: Dict) -> ResponseData: + # TODO: depth parameter + return ResponseData(self.get_obj_ts(url_args["sm_id"], model.Submodel)) From 657b4dd4b1efcb8db9e90717d9c77b62f770c8a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 2 Jul 2020 20:31:02 +0200 Subject: [PATCH 006/157] adapter.http: make http endpoints return a response directly --- basyx/aas/adapter/http/response.py | 65 +++++++++++++++--------------- basyx/aas/adapter/http/wsgi.py | 57 +++++++++++++------------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py index 196976d06..1d131abae 100644 --- a/basyx/aas/adapter/http/response.py +++ b/basyx/aas/adapter/http/response.py @@ -14,7 +14,7 @@ import json from lxml import etree # type: ignore import werkzeug.exceptions -from werkzeug.wrappers import Response +from werkzeug.wrappers import Request, Response from aas import model from aas.adapter.json import json_serialization @@ -48,23 +48,16 @@ def __init__(self, success: bool, is_exception: bool, messages: List[Message]): self.messages = messages -ResponseDataType = Union[Result, model.Referable, List[model.Referable]] - - -class ResponseData(BaseException): - def __init__(self, data: ResponseDataType, http_status_code: int = 200): - self.data = data - self.http_status_code = 200 +ResponseData = Union[Result, model.Referable, List[model.Referable]] class APIResponse(abc.ABC, Response): def __init__(self, data: ResponseData, *args, **kwargs): super().__init__(*args, **kwargs) - self.status_code = data.http_status_code - self.data = self.serialize(data.data) + self.data = self.serialize(data) @abc.abstractmethod - def serialize(self, data: ResponseDataType) -> str: + def serialize(self, data: ResponseData) -> str: pass @@ -72,7 +65,7 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseDataType) -> str: + def serialize(self, data: ResponseData) -> str: return json.dumps(data, cls=json_serialization.AASToJsonEncoder) @@ -80,7 +73,7 @@ class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseDataType) -> str: + def serialize(self, data: ResponseData) -> str: return "" @@ -89,27 +82,35 @@ def __init__(self, *args, content_type="text/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) -""" -A mapping of supported content types to their respective ResponseType. -The first content type in this dict will be preferred if the requester -doesn't specify preferred content types using the HTTP Accept header. -""" -RESPONSE_TYPES: Dict[str, Type[APIResponse]] = { - "application/json": JsonResponse, - "application/xml": XmlResponse, - "text/xml": XmlResponseAlt -} - - -def create_result_response(code: str, text: str, message_type: MessageType, http_status_code: int = 200, - success: bool = False, is_exception: bool = False) -> ResponseData: - message = Message(code, text, message_type) - result = Result(success, is_exception, [message]) - return ResponseData(result, http_status_code) - - def xml_element_to_str(element: etree.Element) -> str: # namespaces will just get assigned a prefix like nsX, where X is a positive integer # "aas" would be a better prefix for the AAS namespace # TODO: find a way to specify a namespace map when serializing return etree.tostring(element, xml_declaration=True, encoding="utf-8") + + +def get_response_type(request: Request) -> Type[APIResponse]: + response_types: Dict[str, Type[APIResponse]] = { + "application/json": JsonResponse, + "application/xml": XmlResponse, + "text/xml": XmlResponseAlt + } + accept_str: Optional[str] = request.headers.get("accept") + if accept_str is None: + # default to json in case unspecified + return JsonResponse + accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) + mime_type = accept.best_match(response_types) + if mime_type is None: + raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " + + ", ".join(response_types.keys())) + return response_types[mime_type] + + +def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, response_type: Type[APIResponse]) \ + -> APIResponse: + success: bool = exception.code < 400 if exception.code is not None else False + message_type = MessageType.INFORMATION if success else MessageType.ERROR + message = Message(type(exception).__name__, exception.description if exception.description is not None else "", + message_type) + return response_type(Result(success, not success, [message])) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py index 6762169ea..9e4a581cb 100644 --- a/basyx/aas/adapter/http/wsgi.py +++ b/basyx/aas/adapter/http/wsgi.py @@ -10,6 +10,7 @@ # specific language governing permissions and limitations under the License. +import urllib.parse import werkzeug from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented from werkzeug.routing import Rule, Submount @@ -17,12 +18,27 @@ from aas import model -from .response import RESPONSE_TYPES, MessageType, ResponseData, create_result_response -from .._generic import identifier_uri_decode, identifier_uri_encode +from .response import APIResponse, get_response_type, http_exception_to_response +from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE from typing import Dict, Iterable, Optional, Type +def identifier_uri_encode(id_: model.Identifier) -> str: + return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") + + +def identifier_uri_decode(id_str: str) -> model.Identifier: + try: + id_type_str, id_ = id_str.split(":", 1) + except ValueError as e: + raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") + id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) + if id_type is None: + raise ValueError(f"Identifier Type '{id_type_str}' is invalid") + return model.Identifier(urllib.parse.unquote(id_), id_type) + + class IdentifierConverter(werkzeug.routing.UnicodeConverter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -55,56 +71,39 @@ def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) return response(environ, start_response) - @classmethod - def preferred_content_type(cls, headers: werkzeug.datastructures.Headers, content_types: Iterable[str]) \ - -> Optional[str]: - accept_str: Optional[str] = headers.get("accept") - if accept_str is None: - # return first content type in case accept http header is not specified - return next(iter(content_types)) - accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) - return accept.best_match(content_types) - # this is not used yet @classmethod def mandatory_request_param(cls, request: Request, param: str) -> str: req_param = request.args.get(param) if req_param is None: - raise create_result_response("mandatory_param_missing", f"Parameter '{param}' is mandatory", - MessageType.ERROR, 400) + raise BadRequest(f"Parameter '{param}' is mandatory") return req_param def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): - raise create_result_response("identifier_not_found", f"No {type_.__name__} with {identifier} found!", - MessageType.ERROR, 404) + raise NotFound(f"No {type_.__name__} with {identifier} found!") return identifiable def handle_request(self, request: Request): adapter = self.url_map.bind_to_environ(request.environ) # determine response content type - preferred_response_type = self.preferred_content_type(request.headers, RESPONSE_TYPES.keys()) - if preferred_response_type is None: - return NotAcceptable(f"This server supports the following content types: " - + ", ".join(RESPONSE_TYPES.keys())) - response_type = RESPONSE_TYPES[preferred_response_type] try: endpoint, values = adapter.match() if endpoint is None: return NotImplemented("This route is not yet implemented.") - return response_type(endpoint(request, values)) + return endpoint(request, values) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them except werkzeug.exceptions.HTTPException as e: - return e - except ResponseData as rd: - return response_type(rd) + return http_exception_to_response(e, get_response_type(request)) - def get_aas(self, request: Request, url_args: Dict) -> ResponseData: + def get_aas(self, request: Request, url_args: Dict) -> APIResponse: # TODO: depth parameter - return ResponseData(self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + response = get_response_type(request) + return response(self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) - def get_sm(self, request: Request, url_args: Dict) -> ResponseData: + def get_sm(self, request: Request, url_args: Dict) -> APIResponse: # TODO: depth parameter - return ResponseData(self.get_obj_ts(url_args["sm_id"], model.Submodel)) + response = get_response_type(request) + return response(self.get_obj_ts(url_args["sm_id"], model.Submodel)) From 05e94e58ff1296ce02ad532f6d907da6c3b10b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 16 Jul 2020 12:44:17 +0200 Subject: [PATCH 007/157] adapter.http: add json/xml result serialization adapter.http: add request body parsing adapter.http: implement all remaining aas routes --- basyx/aas/adapter/http/response.py | 125 +++++++++++-- basyx/aas/adapter/http/wsgi.py | 278 +++++++++++++++++++++++++++-- 2 files changed, 371 insertions(+), 32 deletions(-) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py index 1d131abae..75647845a 100644 --- a/basyx/aas/adapter/http/response.py +++ b/basyx/aas/adapter/http/response.py @@ -20,7 +20,7 @@ from aas.adapter.json import json_serialization from aas.adapter.xml import xml_serialization -from typing import Dict, List, Optional, Type, Union +from typing import Dict, List, Sequence, Type, Union @enum.unique @@ -33,12 +33,15 @@ class MessageType(enum.Enum): FATAL = enum.auto() EXCEPTION = enum.auto() + def __str__(self): + return self.name.capitalize() + class Message: def __init__(self, code: str, text: str, message_type: MessageType = MessageType.UNSPECIFIED): + self.message_type = message_type self.code = code self.text = text - self.message_type = message_type class Result: @@ -48,16 +51,23 @@ def __init__(self, success: bool, is_exception: bool, messages: List[Message]): self.messages = messages -ResponseData = Union[Result, model.Referable, List[model.Referable]] +# not all sequence types are json serializable, but Sequence is covariant, +# which is necessary for List[Submodel] or List[AssetAdministrationShell] to be valid for List[Referable] +ResponseData = Union[Result, model.Referable, Sequence[model.Referable]] + +ResponseDataInternal = Union[Result, model.Referable, List[model.Referable]] class APIResponse(abc.ABC, Response): def __init__(self, data: ResponseData, *args, **kwargs): super().__init__(*args, **kwargs) + # convert possible sequence types to List (see line 54-55) + if isinstance(data, Sequence): + data = list(data) self.data = self.serialize(data) @abc.abstractmethod - def serialize(self, data: ResponseData) -> str: + def serialize(self, data: ResponseDataInternal) -> str: pass @@ -65,16 +75,17 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseData) -> str: - return json.dumps(data, cls=json_serialization.AASToJsonEncoder) + def serialize(self, data: ResponseDataInternal) -> str: + return json.dumps(data, cls=ResultToJsonEncoder if isinstance(data, Result) + else json_serialization.AASToJsonEncoder) class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseData) -> str: - return "" + def serialize(self, data: ResponseDataInternal) -> str: + return xml_element_to_str(response_data_to_xml(data)) class XmlResponseAlt(XmlResponse): @@ -89,18 +100,101 @@ def xml_element_to_str(element: etree.Element) -> str: return etree.tostring(element, xml_declaration=True, encoding="utf-8") +class ResultToJsonEncoder(json.JSONEncoder): + def default(self, obj: object) -> object: + if isinstance(obj, Result): + return result_to_json(obj) + if isinstance(obj, Message): + return message_to_json(obj) + if isinstance(obj, MessageType): + return str(obj) + return super().default(obj) + + +def result_to_json(result: Result) -> Dict[str, object]: + return { + "success": result.success, + "isException": result.is_exception, + "messages": result.messages + } + + +def message_to_json(message: Message) -> Dict[str, object]: + return { + "messageType": message.message_type, + "code": message.code, + "text": message.text + } + + +def response_data_to_xml(data: ResponseDataInternal) -> etree.Element: + if isinstance(data, Result): + return result_to_xml(data) + if isinstance(data, model.Referable): + return referable_to_xml(data) + if isinstance(data, List): + elements: List[etree.Element] = [referable_to_xml(obj) for obj in data] + wrapper = etree.Element("list") + for elem in elements: + wrapper.append(elem) + return wrapper + + +def referable_to_xml(data: model.Referable) -> etree.Element: + # TODO: maybe support more referables + if isinstance(data, model.AssetAdministrationShell): + return xml_serialization.asset_administration_shell_to_xml(data) + if isinstance(data, model.Submodel): + return xml_serialization.submodel_to_xml(data) + if isinstance(data, model.SubmodelElement): + return xml_serialization.submodel_element_to_xml(data) + if isinstance(data, model.ConceptDictionary): + return xml_serialization.concept_dictionary_to_xml(data) + if isinstance(data, model.ConceptDescription): + return xml_serialization.concept_description_to_xml(data) + if isinstance(data, model.View): + return xml_serialization.view_to_xml(data) + if isinstance(data, model.Asset): + return xml_serialization.asset_to_xml(data) + raise TypeError(f"Referable {data} couldn't be serialized to xml (unsupported type)!") + + +def result_to_xml(result: Result) -> etree.Element: + result_elem = etree.Element("result") + success_elem = etree.Element("success") + success_elem.text = xml_serialization.boolean_to_xml(result.success) + is_exception_elem = etree.Element("isException") + is_exception_elem.text = xml_serialization.boolean_to_xml(result.is_exception) + messages_elem = etree.Element("messages") + for message in result.messages: + messages_elem.append(message_to_xml(message)) + result_elem.append(success_elem) + result_elem.append(is_exception_elem) + result_elem.append(messages_elem) + return result_elem + + +def message_to_xml(message: Message) -> etree.Element: + message_elem = etree.Element("message") + message_type_elem = etree.Element("messageType") + message_type_elem.text = str(message.message_type) + code_elem = etree.Element("code") + code_elem.text = message.code + text_elem = etree.Element("text") + text_elem.text = message.text + message_elem.append(message_type_elem) + message_elem.append(code_elem) + message_elem.append(text_elem) + return message_elem + + def get_response_type(request: Request) -> Type[APIResponse]: response_types: Dict[str, Type[APIResponse]] = { "application/json": JsonResponse, "application/xml": XmlResponse, "text/xml": XmlResponseAlt } - accept_str: Optional[str] = request.headers.get("accept") - if accept_str is None: - # default to json in case unspecified - return JsonResponse - accept = werkzeug.http.parse_accept_header(accept_str, werkzeug.datastructures.MIMEAccept) - mime_type = accept.best_match(response_types) + mime_type = request.accept_mimetypes.best_match(response_types) if mime_type is None: raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " + ", ".join(response_types.keys())) @@ -113,4 +207,5 @@ def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, res message_type = MessageType.INFORMATION if success else MessageType.ERROR message = Message(type(exception).__name__, exception.description if exception.description is not None else "", message_type) - return response_type(Result(success, not success, [message])) + return response_type(Result(success, not success, [message]), status=exception.code, + headers=exception.get_headers()) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py index 9e4a581cb..455884e26 100644 --- a/basyx/aas/adapter/http/wsgi.py +++ b/basyx/aas/adapter/http/wsgi.py @@ -10,18 +10,66 @@ # specific language governing permissions and limitations under the License. +import io +import json +from lxml import etree # type: ignore import urllib.parse import werkzeug -from werkzeug.exceptions import BadRequest, NotAcceptable, NotFound, NotImplemented +from werkzeug.exceptions import BadRequest, InternalServerError, NotFound, NotImplemented from werkzeug.routing import Rule, Submount -from werkzeug.wrappers import Request +from werkzeug.wrappers import Request, Response from aas import model - -from .response import APIResponse, get_response_type, http_exception_to_response +from ..xml import xml_deserialization +from ..json import json_deserialization from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE +from .response import get_response_type, http_exception_to_response + +from typing import Dict, Optional, Type + + +def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: + """ + TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent + running out of memory. but it doesn't state how to check the content length + also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema + """ + xml_constructors = { + model.Submodel: xml_deserialization._construct_submodel, + model.View: xml_deserialization._construct_view, + model.ConceptDictionary: xml_deserialization._construct_concept_dictionary, + model.ConceptDescription: xml_deserialization._construct_concept_description, + model.SubmodelElement: xml_deserialization._construct_submodel_element + } + + valid_content_types = ("application/json", "application/xml", "text/xml") + + if request.mimetype not in valid_content_types: + raise werkzeug.exceptions.UnsupportedMediaType(f"Invalid content-type: {request.mimetype}! Supported types: " + + ", ".join(valid_content_types)) + + if request.mimetype == "application/json": + json_data = request.get_data() + try: + rv = json.loads(json_data, cls=json_deserialization.AASFromJsonDecoder) + except json.decoder.JSONDecodeError as e: + raise BadRequest(str(e)) from e + else: + parser = etree.XMLParser(remove_blank_text=True, remove_comments=True) + xml_data = io.BytesIO(request.get_data()) + try: + tree = etree.parse(xml_data, parser) + except etree.XMLSyntaxError as e: + raise BadRequest(str(e)) from e + # TODO: check tag of root element + root = tree.getroot() + try: + rv = xml_constructors[expect_type](root, failsafe=False) + except (KeyError, ValueError) as e: + raise BadRequest(xml_deserialization._exception_to_str(e)) from e -from typing import Dict, Iterable, Optional, Type + assert(isinstance(rv, expect_type)) + return rv def identifier_uri_encode(id_: model.Identifier) -> str: @@ -59,10 +107,33 @@ def __init__(self, object_store: model.AbstractObjectStore): self.url_map = werkzeug.routing.Map([ Submount("/api/v1.0", [ Submount("/shells/", [ - Rule("/aas", methods=["GET"], endpoint=self.get_aas) + Rule("/aas", methods=["GET"], endpoint=self.get_aas), + Submount("/aas", [ + Rule("/asset", methods=["GET"], endpoint=self.get_aas_asset), + Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodels), + Rule("/submodels", methods=["PUT"], endpoint=self.put_aas_submodels), + Rule("/views", methods=["GET"], endpoint=self.get_aas_views), + Rule("/views/", methods=["GET"], + endpoint=self.get_aas_views_specific), + Rule("/views/", methods=["DELETE"], + endpoint=self.delete_aas_views_specific), + Rule("/conceptDictionaries", methods=["GET"], endpoint=self.get_aas_concept_dictionaries), + Rule("/conceptDictionaries", methods=["PUT"], endpoint=self.put_aas_concept_dictionaries), + Rule("/conceptDictionaries/", methods=["GET"], + endpoint=self.get_aas_concept_dictionaries_specific), + Rule("/conceptDictionaries/", methods=["DELETE"], + endpoint=self.delete_aas_concept_dictionaries_specific), + Rule("/submodels/", methods=["GET"], + endpoint=self.get_aas_submodels_specific), + Rule("/submodels/", methods=["DELETE"], + endpoint=self.delete_aas_submodels_specific), + ]) ]), - Submount("/submodels/", [ - Rule("/submodel", methods=["GET"], endpoint=self.get_sm) + Submount("/submodels/", [ + Rule("/submodel", methods=["GET"], endpoint=self.get_submodel), + Submount("/submodel", [ + + ]) ]) ]) ], converters={"identifier": IdentifierConverter}) @@ -85,25 +156,198 @@ def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._I raise NotFound(f"No {type_.__name__} with {identifier} found!") return identifiable + def resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: + try: + return reference.resolve(self.object_store) + except (KeyError, TypeError, model.base.UnexpectedTypeError) as e: + raise InternalServerError(xml_deserialization._exception_to_str(e)) from e + def handle_request(self, request: Request): adapter = self.url_map.bind_to_environ(request.environ) - # determine response content type try: endpoint, values = adapter.match() if endpoint is None: - return NotImplemented("This route is not yet implemented.") + raise NotImplemented("This route is not yet implemented.") return endpoint(request, values) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them + except werkzeug.exceptions.NotAcceptable as e: + return e except werkzeug.exceptions.HTTPException as e: - return http_exception_to_response(e, get_response_type(request)) + try: + # get_response_type() may raise a NotAcceptable error, so we have to handle that + return http_exception_to_response(e, get_response_type(request)) + except werkzeug.exceptions.NotAcceptable as e: + return e + + def get_aas(self, request: Request, url_args: Dict) -> Response: + # TODO: depth parameter + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + return response_t(aas) + + def get_aas_asset(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + asset = self.resolve_reference(aas.asset) + asset.update() + return response_t(asset) + + def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: + # TODO: depth parameter + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + submodels = [self.resolve_reference(ref) for ref in aas.submodel] + for submodel in submodels: + submodel.update() + identification_id: Optional[str] = request.args.get("identification.id") + if identification_id is not None: + # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 + submodels = filter(lambda s: identification_id in s.identification.id, submodels) # type: ignore + semantic_id: Optional[str] = request.args.get("semanticId") + if semantic_id is not None: + # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 + submodels = filter(lambda s: s.semantic_id is not None # type: ignore + and len(s.semantic_id.key) > 0 + and semantic_id in s.semantic_id.key[0].value, submodels) # type: ignore + return response_t(list(submodels)) + + def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + new_submodel = parse_request_body(request, model.Submodel) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + current_submodel = None + for s in iter(self.resolve_reference(ref) for ref in aas.submodel): + if s.identification == new_submodel.identification: + current_submodel = s + break + if current_submodel is None: + aas.submodel.add(model.AASReference.from_referable(new_submodel)) + aas.commit() + not_referenced_submodel = self.object_store.get(new_submodel.identification) + assert(isinstance(not_referenced_submodel, model.Submodel)) + current_submodel = not_referenced_submodel + if current_submodel is not None: + self.object_store.discard(current_submodel) + self.object_store.add(new_submodel) + return response_t(new_submodel, status=201) + + def get_aas_views(self, request: Request, url_args: Dict) -> Response: + # TODO: filter parameter + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + if len(aas.view) == 0: + raise NotFound("No views found!") + return response_t(list(aas.view)) + + def put_aas_views(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + new_view = parse_request_body(request, model.View) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + old_view = aas.view.get(new_view.id_short) + if old_view is not None: + aas.view.discard(old_view) + aas.view.add(new_view) + aas.commit() + return response_t(new_view, status=201) - def get_aas(self, request: Request, url_args: Dict) -> APIResponse: + def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + view = aas.view.get(id_short) + if view is None: + raise NotFound(f"No view with idShort '{id_short}' found!") + view.update() + return response_t(view) + + def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + view = aas.view.get(id_short) + if view is None: + raise NotFound(f"No view with idShort '{id_short}' found!") + view.update() + aas.view.remove(view.id_short) + return Response(status=204) + + def get_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: # TODO: depth parameter - response = get_response_type(request) - return response(self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + if len(aas.concept_dictionary) == 0: + raise NotFound("No concept dictionaries found!") + return response_t(list(aas.concept_dictionary)) + + def put_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + new_concept_dictionary = parse_request_body(request, model.ConceptDictionary) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + old_concept_dictionary = aas.concept_dictionary.get(new_concept_dictionary.id_short) + if old_concept_dictionary is not None: + aas.concept_dictionary.discard(old_concept_dictionary) + aas.concept_dictionary.add(new_concept_dictionary) + aas.commit() + return response_t(new_concept_dictionary, status=201) + + def get_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + concept_dictionary = aas.concept_dictionary.get(id_short) + if concept_dictionary is None: + raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") + concept_dictionary.update() + return response_t(concept_dictionary) + + def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + concept_dictionaries = aas.concept_dictionary.get(id_short) + if concept_dictionaries is None: + raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") + concept_dictionaries.update() + aas.view.remove(concept_dictionaries.id_short) + return Response(status=204) + + def get_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + for submodel in iter(self.resolve_reference(ref) for ref in aas.submodel): + submodel.update() + if submodel.id_short == id_short: + return response_t(submodel) + raise NotFound(f"No submodel with idShort '{id_short}' found!") + + def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: + aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + id_short = url_args["id_short"] + for ref in aas.submodel: + submodel = self.resolve_reference(ref) + submodel.update() + if submodel.id_short == id_short: + aas.submodel.discard(ref) + self.object_store.discard(submodel) + return Response(status=204) + raise NotFound(f"No submodel with idShort '{id_short}' found!") - def get_sm(self, request: Request, url_args: Dict) -> APIResponse: + def get_submodel(self, request: Request, url_args: Dict) -> Response: # TODO: depth parameter - response = get_response_type(request) - return response(self.get_obj_ts(url_args["sm_id"], model.Submodel)) + response_t = get_response_type(request) + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(submodel) From 8ae5de490c571bffb831c4d61c4ed4bb17c0ca87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 25 Jul 2020 17:43:10 +0200 Subject: [PATCH 008/157] adapter.http: add more submodel routes --- basyx/aas/adapter/http/response.py | 7 ++- basyx/aas/adapter/http/wsgi.py | 93 ++++++++++++++++++++++++------ 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py index 75647845a..f6e47af11 100644 --- a/basyx/aas/adapter/http/response.py +++ b/basyx/aas/adapter/http/response.py @@ -203,9 +203,12 @@ def get_response_type(request: Request) -> Type[APIResponse]: def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, response_type: Type[APIResponse]) \ -> APIResponse: + headers = exception.get_headers() + location = exception.get_response().location + if location is not None: + headers.append(("Location", location)) success: bool = exception.code < 400 if exception.code is not None else False message_type = MessageType.INFORMATION if success else MessageType.ERROR message = Message(type(exception).__name__, exception.description if exception.description is not None else "", message_type) - return response_type(Result(success, not success, [message]), status=exception.code, - headers=exception.get_headers()) + return response_type(Result(success, not success, [message]), status=exception.code, headers=headers) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py index 455884e26..60d6d0ed8 100644 --- a/basyx/aas/adapter/http/wsgi.py +++ b/basyx/aas/adapter/http/wsgi.py @@ -25,7 +25,7 @@ from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE from .response import get_response_type, http_exception_to_response -from typing import Dict, Optional, Type +from typing import Dict, List, Optional, Type def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: @@ -88,9 +88,6 @@ def identifier_uri_decode(id_str: str) -> model.Identifier: class IdentifierConverter(werkzeug.routing.UnicodeConverter): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - def to_url(self, value: model.Identifier) -> str: return super().to_url(identifier_uri_encode(value)) @@ -132,7 +129,12 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels/", [ Rule("/submodel", methods=["GET"], endpoint=self.get_submodel), Submount("/submodel", [ - + Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodelElements", methods=["PUT"], endpoint=self.put_submodel_submodel_elements), + Rule("/submodelElements/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_specific_nested), + Rule("/submodelElements/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_specific) ]) ]) ]) @@ -142,14 +144,6 @@ def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) return response(environ, start_response) - # this is not used yet - @classmethod - def mandatory_request_param(cls, request: Request, param: str) -> str: - req_param = request.args.get(param) - if req_param is None: - raise BadRequest(f"Parameter '{param}' is mandatory") - return req_param - def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): @@ -180,6 +174,7 @@ def handle_request(self, request: Request): except werkzeug.exceptions.NotAcceptable as e: return e + # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict) -> Response: # TODO: depth parameter response_t = get_response_type(request) @@ -213,7 +208,10 @@ def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: submodels = filter(lambda s: s.semantic_id is not None # type: ignore and len(s.semantic_id.key) > 0 and semantic_id in s.semantic_id.key[0].value, submodels) # type: ignore - return response_t(list(submodels)) + submodels_list = list(submodels) + if len(submodels_list) == 0: + raise NotFound("No submodels found!") + return response_t(submodels_list) def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) @@ -314,11 +312,11 @@ def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: D aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] - concept_dictionaries = aas.concept_dictionary.get(id_short) - if concept_dictionaries is None: + concept_dictionary = aas.concept_dictionary.get(id_short) + if concept_dictionary is None: raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") - concept_dictionaries.update() - aas.view.remove(concept_dictionaries.id_short) + concept_dictionary.update() + aas.view.remove(concept_dictionary.id_short) return Response(status=204) def get_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: @@ -345,9 +343,68 @@ def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Res return Response(status=204) raise NotFound(f"No submodel with idShort '{id_short}' found!") + # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict) -> Response: # TODO: depth parameter response_t = get_response_type(request) submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() return response_t(submodel) + + def get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: + # TODO: filter parameter + response_t = get_response_type(request) + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_elements = submodel.submodel_element + semantic_id: Optional[str] = request.args.get("semanticId") + if semantic_id is not None: + # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 + submodel_elements = filter(lambda se: se.semantic_id is not None # type: ignore + and len(se.semantic_id.key) > 0 + and semantic_id in se.semantic_id.key[0].value, submodel_elements # type: ignore + ) + submodel_elements_list = list(submodel_elements) + if len(submodel_elements_list) == 0: + raise NotFound("No submodel elements found!") + return response_t(submodel_elements_list) + + def put_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + new_concept_dictionary = parse_request_body(request, model.SubmodelElement) + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + old_submodel_element = submodel.submodel_element.get(new_concept_dictionary.id_short) + if old_submodel_element is not None: + submodel.submodel_element.discard(old_submodel_element) + submodel.submodel_element.add(new_concept_dictionary) + submodel.commit() + return response_t(new_concept_dictionary, status=201) + + def get_submodel_submodel_element_specific_nested(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + id_shorts: List[str] = url_args["id_shorts"].split("/") + submodel_element: model.SubmodelElement = \ + model.SubmodelElementCollectionUnordered("init_wrapper", submodel.submodel_element) + for id_short in id_shorts: + if not isinstance(submodel_element, model.SubmodelElementCollection): + raise NotFound(f"Nested submodel element {submodel_element} is not a submodel element collection!") + try: + submodel_element = submodel_element.value.get_referable(id_short) + except KeyError: + raise NotFound(f"No nested submodel element with idShort '{id_short}' found!") + submodel_element.update() + return response_t(submodel_element) + + def delete_submodel_submodel_element_specific(self, request: Request, url_args: Dict) -> Response: + submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + id_short = url_args["id_short"] + submodel_element = submodel.submodel_element.get(id_short) + if submodel_element is None: + raise NotFound(f"No submodel element with idShort '{id_short}' found!") + submodel_element.update() + submodel.submodel_element.remove(submodel_element.id_short) + return Response(status=204) From 3a953dcefe061163078dc135588937b8a9d89a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 14 Nov 2020 08:40:31 +0100 Subject: [PATCH 009/157] adapter.http: prepare for new api routes from new spec - use stripped object serialization/deserialization - change response format (new spec always returns a result object) --- basyx/aas/adapter/http/response.py | 189 +++++++++++++--------------- basyx/aas/adapter/http/wsgi.py | 192 +++++++++++++---------------- 2 files changed, 175 insertions(+), 206 deletions(-) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py index f6e47af11..54071c9e8 100644 --- a/basyx/aas/adapter/http/response.py +++ b/basyx/aas/adapter/http/response.py @@ -17,14 +17,14 @@ from werkzeug.wrappers import Request, Response from aas import model -from aas.adapter.json import json_serialization -from aas.adapter.xml import xml_serialization +from ..json import StrippedAASToJsonEncoder +from ..xml import xml_serialization -from typing import Dict, List, Sequence, Type, Union +from typing import Dict, Iterable, Optional, Tuple, Type, Union @enum.unique -class MessageType(enum.Enum): +class ErrorType(enum.Enum): UNSPECIFIED = enum.auto() DEBUG = enum.auto() INFORMATION = enum.auto() @@ -37,37 +37,35 @@ def __str__(self): return self.name.capitalize() -class Message: - def __init__(self, code: str, text: str, message_type: MessageType = MessageType.UNSPECIFIED): - self.message_type = message_type +class Error: + def __init__(self, code: str, text: str, type_: ErrorType = ErrorType.UNSPECIFIED): + self.type = type_ self.code = code self.text = text -class Result: - def __init__(self, success: bool, is_exception: bool, messages: List[Message]): - self.success = success - self.is_exception = is_exception - self.messages = messages - +ResultData = Union[object, Tuple[object]] -# not all sequence types are json serializable, but Sequence is covariant, -# which is necessary for List[Submodel] or List[AssetAdministrationShell] to be valid for List[Referable] -ResponseData = Union[Result, model.Referable, Sequence[model.Referable]] -ResponseDataInternal = Union[Result, model.Referable, List[model.Referable]] +class Result: + def __init__(self, data: Optional[Union[ResultData, Error]] = None): + self.success: bool = not isinstance(data, Error) + self.data: Optional[ResultData] = None + self.error: Optional[Error] = None + if isinstance(data, Error): + self.error = data + else: + self.data = data class APIResponse(abc.ABC, Response): - def __init__(self, data: ResponseData, *args, **kwargs): + @abc.abstractmethod + def __init__(self, result: Result, *args, **kwargs): super().__init__(*args, **kwargs) - # convert possible sequence types to List (see line 54-55) - if isinstance(data, Sequence): - data = list(data) - self.data = self.serialize(data) + self.data = self.serialize(result) @abc.abstractmethod - def serialize(self, data: ResponseDataInternal) -> str: + def serialize(self, result: Result) -> str: pass @@ -75,17 +73,18 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseDataInternal) -> str: - return json.dumps(data, cls=ResultToJsonEncoder if isinstance(data, Result) - else json_serialization.AASToJsonEncoder) + def serialize(self, result: Result) -> str: + return json.dumps(result, cls=ResultToJsonEncoder) class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, data: ResponseDataInternal) -> str: - return xml_element_to_str(response_data_to_xml(data)) + def serialize(self, result: Result) -> str: + result_elem = result_to_xml(result, nsmap=xml_serialization.NS_MAP) + etree.cleanup_namespaces(result_elem) + return etree.tostring(result_elem, xml_declaration=True, encoding="utf-8") class XmlResponseAlt(XmlResponse): @@ -93,20 +92,13 @@ def __init__(self, *args, content_type="text/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) -def xml_element_to_str(element: etree.Element) -> str: - # namespaces will just get assigned a prefix like nsX, where X is a positive integer - # "aas" would be a better prefix for the AAS namespace - # TODO: find a way to specify a namespace map when serializing - return etree.tostring(element, xml_declaration=True, encoding="utf-8") - - -class ResultToJsonEncoder(json.JSONEncoder): +class ResultToJsonEncoder(StrippedAASToJsonEncoder): def default(self, obj: object) -> object: if isinstance(obj, Result): return result_to_json(obj) - if isinstance(obj, Message): - return message_to_json(obj) - if isinstance(obj, MessageType): + if isinstance(obj, Error): + return error_to_json(obj) + if isinstance(obj, ErrorType): return str(obj) return super().default(obj) @@ -114,78 +106,72 @@ def default(self, obj: object) -> object: def result_to_json(result: Result) -> Dict[str, object]: return { "success": result.success, - "isException": result.is_exception, - "messages": result.messages + "error": result.error, + "data": result.data } -def message_to_json(message: Message) -> Dict[str, object]: +def error_to_json(error: Error) -> Dict[str, object]: return { - "messageType": message.message_type, - "code": message.code, - "text": message.text + "type": error.type, + "code": error.code, + "text": error.text } -def response_data_to_xml(data: ResponseDataInternal) -> etree.Element: - if isinstance(data, Result): - return result_to_xml(data) - if isinstance(data, model.Referable): - return referable_to_xml(data) - if isinstance(data, List): - elements: List[etree.Element] = [referable_to_xml(obj) for obj in data] - wrapper = etree.Element("list") - for elem in elements: - wrapper.append(elem) - return wrapper - - -def referable_to_xml(data: model.Referable) -> etree.Element: - # TODO: maybe support more referables - if isinstance(data, model.AssetAdministrationShell): - return xml_serialization.asset_administration_shell_to_xml(data) - if isinstance(data, model.Submodel): - return xml_serialization.submodel_to_xml(data) - if isinstance(data, model.SubmodelElement): - return xml_serialization.submodel_element_to_xml(data) - if isinstance(data, model.ConceptDictionary): - return xml_serialization.concept_dictionary_to_xml(data) - if isinstance(data, model.ConceptDescription): - return xml_serialization.concept_description_to_xml(data) - if isinstance(data, model.View): - return xml_serialization.view_to_xml(data) - if isinstance(data, model.Asset): - return xml_serialization.asset_to_xml(data) - raise TypeError(f"Referable {data} couldn't be serialized to xml (unsupported type)!") - - -def result_to_xml(result: Result) -> etree.Element: - result_elem = etree.Element("result") +def result_to_xml(result: Result, **kwargs) -> etree.Element: + result_elem = etree.Element("result", **kwargs) success_elem = etree.Element("success") success_elem.text = xml_serialization.boolean_to_xml(result.success) - is_exception_elem = etree.Element("isException") - is_exception_elem.text = xml_serialization.boolean_to_xml(result.is_exception) - messages_elem = etree.Element("messages") - for message in result.messages: - messages_elem.append(message_to_xml(message)) + error_elem = etree.Element("error") + if result.error is not None: + append_error_elements(error_elem, result.error) + data_elem = etree.Element("data") + if result.data is not None: + for element in result_data_to_xml(result.data): + data_elem.append(element) result_elem.append(success_elem) - result_elem.append(is_exception_elem) - result_elem.append(messages_elem) + result_elem.append(error_elem) + result_elem.append(data_elem) return result_elem -def message_to_xml(message: Message) -> etree.Element: - message_elem = etree.Element("message") - message_type_elem = etree.Element("messageType") - message_type_elem.text = str(message.message_type) +def append_error_elements(element: etree.Element, error: Error) -> None: + type_elem = etree.Element("type") + type_elem.text = str(error.type) code_elem = etree.Element("code") - code_elem.text = message.code + code_elem.text = error.code text_elem = etree.Element("text") - text_elem.text = message.text - message_elem.append(message_type_elem) - message_elem.append(code_elem) - message_elem.append(text_elem) - return message_elem + text_elem.text = error.text + element.append(type_elem) + element.append(code_elem) + element.append(text_elem) + + +def result_data_to_xml(data: ResultData) -> Iterable[etree.Element]: + if not isinstance(data, tuple): + data = (data,) + for obj in data: + yield aas_object_to_xml(obj) + + +def aas_object_to_xml(obj: object) -> etree.Element: + if isinstance(obj, model.AssetAdministrationShell): + return xml_serialization.asset_administration_shell_to_xml(obj) + if isinstance(obj, model.Reference): + return xml_serialization.reference_to_xml(obj) + if isinstance(obj, model.View): + return xml_serialization.view_to_xml(obj) + if isinstance(obj, model.Submodel): + return xml_serialization.submodel_to_xml(obj) + # TODO: xml serialization needs a constraint_to_xml() function + if isinstance(obj, model.Qualifier): + return xml_serialization.qualifier_to_xml(obj) + if isinstance(obj, model.Formula): + return xml_serialization.formula_to_xml(obj) + if isinstance(obj, model.SubmodelElement): + return xml_serialization.submodel_element_to_xml(obj) + raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") def get_response_type(request: Request) -> Type[APIResponse]: @@ -207,8 +193,9 @@ def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, res location = exception.get_response().location if location is not None: headers.append(("Location", location)) - success: bool = exception.code < 400 if exception.code is not None else False - message_type = MessageType.INFORMATION if success else MessageType.ERROR - message = Message(type(exception).__name__, exception.description if exception.description is not None else "", - message_type) - return response_type(Result(success, not success, [message]), status=exception.code, headers=headers) + result = Result() + if exception.code and exception.code >= 400: + error = Error(type(exception).__name__, exception.description if exception.description is not None else "", + ErrorType.ERROR) + result = Result(error) + return response_type(result, status=exception.code, headers=headers) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py index 60d6d0ed8..de3e8ae6b 100644 --- a/basyx/aas/adapter/http/wsgi.py +++ b/basyx/aas/adapter/http/wsgi.py @@ -12,7 +12,6 @@ import io import json -from lxml import etree # type: ignore import urllib.parse import werkzeug from werkzeug.exceptions import BadRequest, InternalServerError, NotFound, NotImplemented @@ -20,10 +19,10 @@ from werkzeug.wrappers import Request, Response from aas import model -from ..xml import xml_deserialization -from ..json import json_deserialization +from ..xml import XMLConstructables, read_aas_xml_element +from ..json import StrippedAASFromJsonDecoder from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from .response import get_response_type, http_exception_to_response +from .response import Result, get_response_type, http_exception_to_response from typing import Dict, List, Optional, Type @@ -35,13 +34,15 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema """ xml_constructors = { - model.Submodel: xml_deserialization._construct_submodel, - model.View: xml_deserialization._construct_view, - model.ConceptDictionary: xml_deserialization._construct_concept_dictionary, - model.ConceptDescription: xml_deserialization._construct_concept_description, - model.SubmodelElement: xml_deserialization._construct_submodel_element + model.AASReference: XMLConstructables.AAS_REFERENCE, + model.View: XMLConstructables.VIEW, + model.Constraint: XMLConstructables.CONSTRAINT, + model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT } + if expect_type not in xml_constructors: + raise TypeError(f"Parsing {expect_type} is not supported!") + valid_content_types = ("application/json", "application/xml", "text/xml") if request.mimetype not in valid_content_types: @@ -51,28 +52,19 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m if request.mimetype == "application/json": json_data = request.get_data() try: - rv = json.loads(json_data, cls=json_deserialization.AASFromJsonDecoder) + rv = json.loads(json_data, cls=StrippedAASFromJsonDecoder) except json.decoder.JSONDecodeError as e: raise BadRequest(str(e)) from e else: - parser = etree.XMLParser(remove_blank_text=True, remove_comments=True) xml_data = io.BytesIO(request.get_data()) - try: - tree = etree.parse(xml_data, parser) - except etree.XMLSyntaxError as e: - raise BadRequest(str(e)) from e - # TODO: check tag of root element - root = tree.getroot() - try: - rv = xml_constructors[expect_type](root, failsafe=False) - except (KeyError, ValueError) as e: - raise BadRequest(xml_deserialization._exception_to_str(e)) from e + rv = read_aas_xml_element(xml_data, xml_constructors[expect_type], stripped=True) - assert(isinstance(rv, expect_type)) + assert isinstance(rv, expect_type) return rv def identifier_uri_encode(id_: model.Identifier) -> str: + # TODO: replace urllib with urllib3 if we're using it anyways? return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") @@ -102,40 +94,36 @@ class WSGIApp: def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ - Submount("/api/v1.0", [ - Submount("/shells/", [ - Rule("/aas", methods=["GET"], endpoint=self.get_aas), - Submount("/aas", [ - Rule("/asset", methods=["GET"], endpoint=self.get_aas_asset), - Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodels), - Rule("/submodels", methods=["PUT"], endpoint=self.put_aas_submodels), - Rule("/views", methods=["GET"], endpoint=self.get_aas_views), - Rule("/views/", methods=["GET"], - endpoint=self.get_aas_views_specific), - Rule("/views/", methods=["DELETE"], - endpoint=self.delete_aas_views_specific), - Rule("/conceptDictionaries", methods=["GET"], endpoint=self.get_aas_concept_dictionaries), - Rule("/conceptDictionaries", methods=["PUT"], endpoint=self.put_aas_concept_dictionaries), - Rule("/conceptDictionaries/", methods=["GET"], - endpoint=self.get_aas_concept_dictionaries_specific), - Rule("/conceptDictionaries/", methods=["DELETE"], - endpoint=self.delete_aas_concept_dictionaries_specific), - Rule("/submodels/", methods=["GET"], - endpoint=self.get_aas_submodels_specific), - Rule("/submodels/", methods=["DELETE"], - endpoint=self.delete_aas_submodels_specific), - ]) + Submount("/api/v1", [ + Rule("/aas/", endpoint=self.get_aas), + Submount("/aas/", [ + Rule("/asset", methods=["GET"], endpoint=self.get_aas_asset), + Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodels), + Rule("/submodels", methods=["PUT"], endpoint=self.put_aas_submodels), + Rule("/views", methods=["GET"], endpoint=self.get_aas_views), + Rule("/views/", methods=["GET"], + endpoint=self.get_aas_views_specific), + Rule("/views/", methods=["DELETE"], + endpoint=self.delete_aas_views_specific), + Rule("/conceptDictionaries", methods=["GET"], endpoint=self.get_aas_concept_dictionaries), + Rule("/conceptDictionaries", methods=["PUT"], endpoint=self.put_aas_concept_dictionaries), + Rule("/conceptDictionaries/", methods=["GET"], + endpoint=self.get_aas_concept_dictionaries_specific), + Rule("/conceptDictionaries/", methods=["DELETE"], + endpoint=self.delete_aas_concept_dictionaries_specific), + Rule("/submodels/", methods=["GET"], + endpoint=self.get_aas_submodels_specific), + Rule("/submodels/", methods=["DELETE"], + endpoint=self.delete_aas_submodels_specific), ]), + Rule("/submodels/", endpoint=self.get_submodel), Submount("/submodels/", [ - Rule("/submodel", methods=["GET"], endpoint=self.get_submodel), - Submount("/submodel", [ - Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/submodelElements", methods=["PUT"], endpoint=self.put_submodel_submodel_elements), - Rule("/submodelElements/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_specific_nested), - Rule("/submodelElements/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_specific) - ]) + Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodelElements", methods=["PUT"], endpoint=self.put_submodel_submodel_elements), + Rule("/submodelElements/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_specific_nested), + Rule("/submodelElements/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_specific) ]) ]) ], converters={"identifier": IdentifierConverter}) @@ -144,17 +132,17 @@ def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) return response(environ, start_response) - def get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: + def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): raise NotFound(f"No {type_.__name__} with {identifier} found!") return identifiable - def resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: + def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: try: return reference.resolve(self.object_store) - except (KeyError, TypeError, model.base.UnexpectedTypeError) as e: - raise InternalServerError(xml_deserialization._exception_to_str(e)) from e + except (KeyError, TypeError, model.UnexpectedTypeError) as e: + raise InternalServerError(str(e)) from e def handle_request(self, request: Request): adapter = self.url_map.bind_to_environ(request.environ) @@ -176,26 +164,24 @@ def handle_request(self, request: Request): # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict) -> Response: - # TODO: depth parameter response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - return response_t(aas) + return response_t(Result(aas)) def get_aas_asset(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - asset = self.resolve_reference(aas.asset) + asset = self._resolve_reference(aas.asset) asset.update() - return response_t(asset) + return response_t(Result(asset)) def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: - # TODO: depth parameter response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - submodels = [self.resolve_reference(ref) for ref in aas.submodel] + submodels = [self._resolve_reference(ref) for ref in aas.submodel] for submodel in submodels: submodel.update() identification_id: Optional[str] = request.args.get("identification.id") @@ -211,15 +197,15 @@ def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: submodels_list = list(submodels) if len(submodels_list) == 0: raise NotFound("No submodels found!") - return response_t(submodels_list) + return response_t(Result(submodels_list)) def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) new_submodel = parse_request_body(request, model.Submodel) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() current_submodel = None - for s in iter(self.resolve_reference(ref) for ref in aas.submodel): + for s in iter(self._resolve_reference(ref) for ref in aas.submodel): if s.identification == new_submodel.identification: current_submodel = s break @@ -232,41 +218,40 @@ def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: if current_submodel is not None: self.object_store.discard(current_submodel) self.object_store.add(new_submodel) - return response_t(new_submodel, status=201) + return response_t(Result(new_submodel), status=201) def get_aas_views(self, request: Request, url_args: Dict) -> Response: - # TODO: filter parameter response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) if len(aas.view) == 0: raise NotFound("No views found!") - return response_t(list(aas.view)) + return response_t(Result((aas.view,))) def put_aas_views(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) new_view = parse_request_body(request, model.View) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() old_view = aas.view.get(new_view.id_short) if old_view is not None: aas.view.discard(old_view) aas.view.add(new_view) aas.commit() - return response_t(new_view, status=201) + return response_t(Result(new_view), status=201) def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] view = aas.view.get(id_short) if view is None: raise NotFound(f"No view with idShort '{id_short}' found!") view.update() - return response_t(view) + return response_t(Result(view)) def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] view = aas.view.get(id_short) @@ -277,39 +262,38 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Respons return Response(status=204) def get_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: - # TODO: depth parameter response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() if len(aas.concept_dictionary) == 0: raise NotFound("No concept dictionaries found!") - return response_t(list(aas.concept_dictionary)) + return response_t(Result((aas.concept_dictionary,))) def put_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) new_concept_dictionary = parse_request_body(request, model.ConceptDictionary) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() old_concept_dictionary = aas.concept_dictionary.get(new_concept_dictionary.id_short) if old_concept_dictionary is not None: aas.concept_dictionary.discard(old_concept_dictionary) aas.concept_dictionary.add(new_concept_dictionary) aas.commit() - return response_t(new_concept_dictionary, status=201) + return response_t(Result((new_concept_dictionary,)), status=201) def get_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] concept_dictionary = aas.concept_dictionary.get(id_short) if concept_dictionary is None: raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") concept_dictionary.update() - return response_t(concept_dictionary) + return response_t(Result(concept_dictionary)) def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] concept_dictionary = aas.concept_dictionary.get(id_short) @@ -321,21 +305,21 @@ def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: D def get_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] - for submodel in iter(self.resolve_reference(ref) for ref in aas.submodel): + for submodel in iter(self._resolve_reference(ref) for ref in aas.submodel): submodel.update() if submodel.id_short == id_short: - return response_t(submodel) + return response_t(Result(submodel)) raise NotFound(f"No submodel with idShort '{id_short}' found!") def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: - aas = self.get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() id_short = url_args["id_short"] for ref in aas.submodel: - submodel = self.resolve_reference(ref) + submodel = self._resolve_reference(ref) submodel.update() if submodel.id_short == id_short: aas.submodel.discard(ref) @@ -345,16 +329,14 @@ def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Res # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict) -> Response: - # TODO: depth parameter response_t = get_response_type(request) - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - return response_t(submodel) + return response_t(Result(submodel)) def get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: - # TODO: filter parameter response_t = get_response_type(request) - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() submodel_elements = submodel.submodel_element semantic_id: Optional[str] = request.args.get("semanticId") @@ -364,26 +346,26 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Re and len(se.semantic_id.key) > 0 and semantic_id in se.semantic_id.key[0].value, submodel_elements # type: ignore ) - submodel_elements_list = list(submodel_elements) - if len(submodel_elements_list) == 0: + submodel_elements_tuple = (submodel_elements,) + if len(submodel_elements_tuple) == 0: raise NotFound("No submodel elements found!") - return response_t(submodel_elements_list) + return response_t(Result(submodel_elements_tuple)) def put_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) new_concept_dictionary = parse_request_body(request, model.SubmodelElement) - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() old_submodel_element = submodel.submodel_element.get(new_concept_dictionary.id_short) if old_submodel_element is not None: submodel.submodel_element.discard(old_submodel_element) submodel.submodel_element.add(new_concept_dictionary) submodel.commit() - return response_t(new_concept_dictionary, status=201) + return response_t(Result(new_concept_dictionary), status=201) def get_submodel_submodel_element_specific_nested(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() id_shorts: List[str] = url_args["id_shorts"].split("/") submodel_element: model.SubmodelElement = \ @@ -396,10 +378,10 @@ def get_submodel_submodel_element_specific_nested(self, request: Request, url_ar except KeyError: raise NotFound(f"No nested submodel element with idShort '{id_short}' found!") submodel_element.update() - return response_t(submodel_element) + return response_t(Result(submodel_element)) def delete_submodel_submodel_element_specific(self, request: Request, url_args: Dict) -> Response: - submodel = self.get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() id_short = url_args["id_short"] submodel_element = submodel.submodel_element.get(id_short) From a3856cbbb781cb28b7ae2c20488ceb5617712fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 18 Nov 2020 23:33:16 +0100 Subject: [PATCH 010/157] adapter.http: move code from folder to file http.py adapter.http: remove old routes, add a few new aas and submodel routes --- basyx/aas/adapter/http.py | 398 +++++++++++++++++++++++++++++ basyx/aas/adapter/http/__init__.py | 12 - basyx/aas/adapter/http/__main__.py | 17 -- basyx/aas/adapter/http/response.py | 201 --------------- basyx/aas/adapter/http/wsgi.py | 392 ---------------------------- 5 files changed, 398 insertions(+), 622 deletions(-) create mode 100644 basyx/aas/adapter/http.py delete mode 100644 basyx/aas/adapter/http/__init__.py delete mode 100644 basyx/aas/adapter/http/__main__.py delete mode 100644 basyx/aas/adapter/http/response.py delete mode 100644 basyx/aas/adapter/http/wsgi.py diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py new file mode 100644 index 000000000..1b5fe1bdd --- /dev/null +++ b/basyx/aas/adapter/http.py @@ -0,0 +1,398 @@ +# Copyright 2020 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + + +import abc +import enum +import io +import json +from lxml import etree # type: ignore +import urllib.parse +import werkzeug +from werkzeug.exceptions import BadRequest, Conflict, InternalServerError, NotFound, NotImplemented +from werkzeug.routing import Rule, Submount +from werkzeug.wrappers import Request, Response + +from aas import model +from .xml import XMLConstructables, read_aas_xml_element, xml_serialization +from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder +from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE + +from typing import Dict, Iterable, List, Optional, Tuple, Type, Union + + +@enum.unique +class ErrorType(enum.Enum): + UNSPECIFIED = enum.auto() + DEBUG = enum.auto() + INFORMATION = enum.auto() + WARNING = enum.auto() + ERROR = enum.auto() + FATAL = enum.auto() + EXCEPTION = enum.auto() + + def __str__(self): + return self.name.capitalize() + + +class Error: + def __init__(self, code: str, text: str, type_: ErrorType = ErrorType.UNSPECIFIED): + self.type = type_ + self.code = code + self.text = text + + +ResultData = Union[object, Tuple[object, ...]] + + +class Result: + def __init__(self, data: Optional[Union[ResultData, Error]]): + # the following is True when data is None, which is the expected behavior + self.success: bool = not isinstance(data, Error) + self.data: Optional[ResultData] = None + self.error: Optional[Error] = None + if isinstance(data, Error): + self.error = data + else: + self.data = data + + +class ResultToJsonEncoder(StrippedAASToJsonEncoder): + @classmethod + def _result_to_json(cls, result: Result) -> Dict[str, object]: + return { + "success": result.success, + "error": result.error, + "data": result.data + } + + @classmethod + def _error_to_json(cls, error: Error) -> Dict[str, object]: + return { + "type": error.type, + "code": error.code, + "text": error.text + } + + def default(self, obj: object) -> object: + if isinstance(obj, Result): + return self._result_to_json(obj) + if isinstance(obj, Error): + return self._error_to_json(obj) + if isinstance(obj, ErrorType): + return str(obj) + return super().default(obj) + + +class APIResponse(abc.ABC, werkzeug.wrappers.Response): + @abc.abstractmethod + def __init__(self, result: Result, *args, **kwargs): + super().__init__(*args, **kwargs) + self.data = self.serialize(result) + + @abc.abstractmethod + def serialize(self, result: Result) -> str: + pass + + +class JsonResponse(APIResponse): + def __init__(self, *args, content_type="application/json", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize(self, result: Result) -> str: + return json.dumps(result, cls=ResultToJsonEncoder) + + +class XmlResponse(APIResponse): + def __init__(self, *args, content_type="application/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + def serialize(self, result: Result) -> str: + result_elem = result_to_xml(result, nsmap=xml_serialization.NS_MAP) + etree.cleanup_namespaces(result_elem) + return etree.tostring(result_elem, xml_declaration=True, encoding="utf-8") + + +class XmlResponseAlt(XmlResponse): + def __init__(self, *args, content_type="text/xml", **kwargs): + super().__init__(*args, **kwargs, content_type=content_type) + + +def result_to_xml(result: Result, **kwargs) -> etree.Element: + result_elem = etree.Element("result", **kwargs) + success_elem = etree.Element("success") + success_elem.text = xml_serialization.boolean_to_xml(result.success) + if result.error is None: + error_elem = etree.Element("error") + else: + error_elem = error_to_xml(result.error) + data_elem = etree.Element("data") + if result.data is not None: + for element in result_data_to_xml(result.data): + data_elem.append(element) + result_elem.append(success_elem) + result_elem.append(error_elem) + result_elem.append(data_elem) + return result_elem + + +def error_to_xml(error: Error) -> etree.Element: + error_elem = etree.Element("error") + type_elem = etree.Element("type") + type_elem.text = str(error.type) + code_elem = etree.Element("code") + code_elem.text = error.code + text_elem = etree.Element("text") + text_elem.text = error.text + error_elem.append(type_elem) + error_elem.append(code_elem) + error_elem.append(text_elem) + return error_elem + + +def result_data_to_xml(data: ResultData) -> Iterable[etree.Element]: + # for xml we can just append multiple elements to the data element + # so multiple elements will be handled the same as a single element + if not isinstance(data, tuple): + data = (data,) + for obj in data: + yield aas_object_to_xml(obj) + + +def aas_object_to_xml(obj: object) -> etree.Element: + # TODO: a similar function should be implemented in the xml serialization + if isinstance(obj, model.AssetAdministrationShell): + return xml_serialization.asset_administration_shell_to_xml(obj) + if isinstance(obj, model.Reference): + return xml_serialization.reference_to_xml(obj) + if isinstance(obj, model.View): + return xml_serialization.view_to_xml(obj) + if isinstance(obj, model.Submodel): + return xml_serialization.submodel_to_xml(obj) + # TODO: xml serialization needs a constraint_to_xml() function + if isinstance(obj, model.Qualifier): + return xml_serialization.qualifier_to_xml(obj) + if isinstance(obj, model.Formula): + return xml_serialization.formula_to_xml(obj) + if isinstance(obj, model.SubmodelElement): + return xml_serialization.submodel_element_to_xml(obj) + raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") + + +def get_response_type(request: Request) -> Type[APIResponse]: + response_types: Dict[str, Type[APIResponse]] = { + "application/json": JsonResponse, + "application/xml": XmlResponse, + "text/xml": XmlResponseAlt + } + mime_type = request.accept_mimetypes.best_match(response_types) + if mime_type is None: + raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " + + ", ".join(response_types.keys())) + return response_types[mime_type] + + +def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, response_type: Type[APIResponse]) \ + -> APIResponse: + headers = exception.get_headers() + location = exception.get_response().location + if location is not None: + headers.append(("Location", location)) + if exception.code and exception.code >= 400: + error = Error(type(exception).__name__, exception.description if exception.description is not None else "", + ErrorType.ERROR) + result = Result(error) + else: + result = Result(None) + return response_type(result, status=exception.code, headers=headers) + + +def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: + """ + TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent + running out of memory. but it doesn't state how to check the content length + also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema + """ + type_constructables_map = { + model.AASReference: XMLConstructables.AAS_REFERENCE, + model.View: XMLConstructables.VIEW, + model.Constraint: XMLConstructables.CONSTRAINT, + model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT + } + + if expect_type not in type_constructables_map: + raise TypeError(f"Parsing {expect_type} is not supported!") + + valid_content_types = ("application/json", "application/xml", "text/xml") + + if request.mimetype not in valid_content_types: + raise werkzeug.exceptions.UnsupportedMediaType(f"Invalid content-type: {request.mimetype}! Supported types: " + + ", ".join(valid_content_types)) + + try: + if request.mimetype == "application/json": + rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) + else: + xml_data = io.BytesIO(request.get_data()) + rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) + except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError) as e: + raise BadRequest(str(e)) from e + + assert isinstance(rv, expect_type) + return rv + + +def identifier_uri_encode(id_: model.Identifier) -> str: + # TODO: replace urllib with urllib3 if we're using it anyways? + return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") + + +def identifier_uri_decode(id_str: str) -> model.Identifier: + try: + id_type_str, id_ = id_str.split(":", 1) + except ValueError: + raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") + id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) + if id_type is None: + raise ValueError(f"Identifier Type '{id_type_str}' is invalid") + return model.Identifier(urllib.parse.unquote(id_), id_type) + + +class IdentifierConverter(werkzeug.routing.UnicodeConverter): + def to_url(self, value: model.Identifier) -> str: + return super().to_url(identifier_uri_encode(value)) + + def to_python(self, value: str) -> model.Identifier: + try: + return identifier_uri_decode(super().to_python(value)) + except ValueError as e: + raise BadRequest(str(e)) + + +class WSGIApp: + def __init__(self, object_store: model.AbstractObjectStore): + self.object_store: model.AbstractObjectStore = object_store + self.url_map = werkzeug.routing.Map([ + Submount("/api/v1", [ + Rule("/aas/", endpoint=self.get_aas), + Submount("/aas/", [ + Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("/submodels", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("/submodels/", methods=["GET"], + endpoint=self.get_aas_submodel_refs_specific), + Rule("/submodels/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific), + ]), + Rule("/submodels/", endpoint=self.get_submodel), + ]) + ], converters={"identifier": IdentifierConverter}) + + def __call__(self, environ, start_response): + response = self.handle_request(Request(environ)) + return response(environ, start_response) + + def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: + identifiable = self.object_store.get(identifier) + if not isinstance(identifiable, type_): + raise NotFound(f"No {type_.__name__} with {identifier} found!") + return identifiable + + def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: + try: + return reference.resolve(self.object_store) + except (KeyError, TypeError, model.UnexpectedTypeError) as e: + raise InternalServerError(str(e)) from e + + @classmethod + def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdministrationShell, + sm_identifier: model.Identifier) \ + -> model.AASReference[model.Submodel]: + for sm_ref in aas.submodel: + if sm_ref.get_identifier() == sm_identifier: + return sm_ref + raise NotFound(f"No reference to submodel with {sm_identifier} found!") + + def handle_request(self, request: Request): + adapter = self.url_map.bind_to_environ(request.environ) + try: + endpoint, values = adapter.match() + if endpoint is None: + raise NotImplemented("This route is not yet implemented.") + return endpoint(request, values) + # any raised error that leaves this function will cause a 500 internal server error + # so catch raised http exceptions and return them + except werkzeug.exceptions.NotAcceptable as e: + return e + except werkzeug.exceptions.HTTPException as e: + try: + # get_response_type() may raise a NotAcceptable error, so we have to handle that + return http_exception_to_response(e, get_response_type(request)) + except werkzeug.exceptions.NotAcceptable as e: + return e + + # --------- AAS ROUTES --------- + def get_aas(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + return response_t(Result(aas)) + + def get_aas_submodel_refs(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + return response_t(Result(tuple(aas.submodel))) + + def post_aas_submodel_refs(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = parse_request_body(request, model.AASReference) # type: ignore + assert isinstance(sm_ref, model.AASReference) + if sm_ref in aas.submodel: + raise Conflict(f"{sm_ref!r} already exists!") + # TODO: check if reference references a non-existant submodel? + aas.submodel.add(sm_ref) + aas.commit() + return response_t(Result(sm_ref), status=201) + + def get_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) + return response_t(Result(sm_ref)) + + def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) + # use remove(sm_ref) because it raises a KeyError if sm_ref is not present + # sm_ref must be present because _get_aas_submodel_reference_by_submodel_identifier() found it there + # so if sm_ref is not in aas.submodel, this implementation is bugged and the raised KeyError will result + # in an InternalServerError + aas.submodel.remove(sm_ref) + aas.commit() + return response_t(Result(None)) + + # --------- SUBMODEL ROUTES --------- + def get_submodel(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(Result(submodel)) + + +if __name__ == "__main__": + from werkzeug.serving import run_simple + from aas.examples.data.example_aas import create_full_example + run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) diff --git a/basyx/aas/adapter/http/__init__.py b/basyx/aas/adapter/http/__init__.py deleted file mode 100644 index 27808a859..000000000 --- a/basyx/aas/adapter/http/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -from .wsgi import WSGIApp diff --git a/basyx/aas/adapter/http/__main__.py b/basyx/aas/adapter/http/__main__.py deleted file mode 100644 index 59bf98bb3..000000000 --- a/basyx/aas/adapter/http/__main__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -from werkzeug.serving import run_simple - -from aas.examples.data.example_aas import create_full_example -from . import WSGIApp - -run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) diff --git a/basyx/aas/adapter/http/response.py b/basyx/aas/adapter/http/response.py deleted file mode 100644 index 54071c9e8..000000000 --- a/basyx/aas/adapter/http/response.py +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -import abc -import enum -import json -from lxml import etree # type: ignore -import werkzeug.exceptions -from werkzeug.wrappers import Request, Response - -from aas import model -from ..json import StrippedAASToJsonEncoder -from ..xml import xml_serialization - -from typing import Dict, Iterable, Optional, Tuple, Type, Union - - -@enum.unique -class ErrorType(enum.Enum): - UNSPECIFIED = enum.auto() - DEBUG = enum.auto() - INFORMATION = enum.auto() - WARNING = enum.auto() - ERROR = enum.auto() - FATAL = enum.auto() - EXCEPTION = enum.auto() - - def __str__(self): - return self.name.capitalize() - - -class Error: - def __init__(self, code: str, text: str, type_: ErrorType = ErrorType.UNSPECIFIED): - self.type = type_ - self.code = code - self.text = text - - -ResultData = Union[object, Tuple[object]] - - -class Result: - def __init__(self, data: Optional[Union[ResultData, Error]] = None): - self.success: bool = not isinstance(data, Error) - self.data: Optional[ResultData] = None - self.error: Optional[Error] = None - if isinstance(data, Error): - self.error = data - else: - self.data = data - - -class APIResponse(abc.ABC, Response): - @abc.abstractmethod - def __init__(self, result: Result, *args, **kwargs): - super().__init__(*args, **kwargs) - self.data = self.serialize(result) - - @abc.abstractmethod - def serialize(self, result: Result) -> str: - pass - - -class JsonResponse(APIResponse): - def __init__(self, *args, content_type="application/json", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - def serialize(self, result: Result) -> str: - return json.dumps(result, cls=ResultToJsonEncoder) - - -class XmlResponse(APIResponse): - def __init__(self, *args, content_type="application/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - def serialize(self, result: Result) -> str: - result_elem = result_to_xml(result, nsmap=xml_serialization.NS_MAP) - etree.cleanup_namespaces(result_elem) - return etree.tostring(result_elem, xml_declaration=True, encoding="utf-8") - - -class XmlResponseAlt(XmlResponse): - def __init__(self, *args, content_type="text/xml", **kwargs): - super().__init__(*args, **kwargs, content_type=content_type) - - -class ResultToJsonEncoder(StrippedAASToJsonEncoder): - def default(self, obj: object) -> object: - if isinstance(obj, Result): - return result_to_json(obj) - if isinstance(obj, Error): - return error_to_json(obj) - if isinstance(obj, ErrorType): - return str(obj) - return super().default(obj) - - -def result_to_json(result: Result) -> Dict[str, object]: - return { - "success": result.success, - "error": result.error, - "data": result.data - } - - -def error_to_json(error: Error) -> Dict[str, object]: - return { - "type": error.type, - "code": error.code, - "text": error.text - } - - -def result_to_xml(result: Result, **kwargs) -> etree.Element: - result_elem = etree.Element("result", **kwargs) - success_elem = etree.Element("success") - success_elem.text = xml_serialization.boolean_to_xml(result.success) - error_elem = etree.Element("error") - if result.error is not None: - append_error_elements(error_elem, result.error) - data_elem = etree.Element("data") - if result.data is not None: - for element in result_data_to_xml(result.data): - data_elem.append(element) - result_elem.append(success_elem) - result_elem.append(error_elem) - result_elem.append(data_elem) - return result_elem - - -def append_error_elements(element: etree.Element, error: Error) -> None: - type_elem = etree.Element("type") - type_elem.text = str(error.type) - code_elem = etree.Element("code") - code_elem.text = error.code - text_elem = etree.Element("text") - text_elem.text = error.text - element.append(type_elem) - element.append(code_elem) - element.append(text_elem) - - -def result_data_to_xml(data: ResultData) -> Iterable[etree.Element]: - if not isinstance(data, tuple): - data = (data,) - for obj in data: - yield aas_object_to_xml(obj) - - -def aas_object_to_xml(obj: object) -> etree.Element: - if isinstance(obj, model.AssetAdministrationShell): - return xml_serialization.asset_administration_shell_to_xml(obj) - if isinstance(obj, model.Reference): - return xml_serialization.reference_to_xml(obj) - if isinstance(obj, model.View): - return xml_serialization.view_to_xml(obj) - if isinstance(obj, model.Submodel): - return xml_serialization.submodel_to_xml(obj) - # TODO: xml serialization needs a constraint_to_xml() function - if isinstance(obj, model.Qualifier): - return xml_serialization.qualifier_to_xml(obj) - if isinstance(obj, model.Formula): - return xml_serialization.formula_to_xml(obj) - if isinstance(obj, model.SubmodelElement): - return xml_serialization.submodel_element_to_xml(obj) - raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") - - -def get_response_type(request: Request) -> Type[APIResponse]: - response_types: Dict[str, Type[APIResponse]] = { - "application/json": JsonResponse, - "application/xml": XmlResponse, - "text/xml": XmlResponseAlt - } - mime_type = request.accept_mimetypes.best_match(response_types) - if mime_type is None: - raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " - + ", ".join(response_types.keys())) - return response_types[mime_type] - - -def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, response_type: Type[APIResponse]) \ - -> APIResponse: - headers = exception.get_headers() - location = exception.get_response().location - if location is not None: - headers.append(("Location", location)) - result = Result() - if exception.code and exception.code >= 400: - error = Error(type(exception).__name__, exception.description if exception.description is not None else "", - ErrorType.ERROR) - result = Result(error) - return response_type(result, status=exception.code, headers=headers) diff --git a/basyx/aas/adapter/http/wsgi.py b/basyx/aas/adapter/http/wsgi.py deleted file mode 100644 index de3e8ae6b..000000000 --- a/basyx/aas/adapter/http/wsgi.py +++ /dev/null @@ -1,392 +0,0 @@ -# Copyright 2020 PyI40AAS Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - - -import io -import json -import urllib.parse -import werkzeug -from werkzeug.exceptions import BadRequest, InternalServerError, NotFound, NotImplemented -from werkzeug.routing import Rule, Submount -from werkzeug.wrappers import Request, Response - -from aas import model -from ..xml import XMLConstructables, read_aas_xml_element -from ..json import StrippedAASFromJsonDecoder -from .._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from .response import Result, get_response_type, http_exception_to_response - -from typing import Dict, List, Optional, Type - - -def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: - """ - TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent - running out of memory. but it doesn't state how to check the content length - also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema - """ - xml_constructors = { - model.AASReference: XMLConstructables.AAS_REFERENCE, - model.View: XMLConstructables.VIEW, - model.Constraint: XMLConstructables.CONSTRAINT, - model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT - } - - if expect_type not in xml_constructors: - raise TypeError(f"Parsing {expect_type} is not supported!") - - valid_content_types = ("application/json", "application/xml", "text/xml") - - if request.mimetype not in valid_content_types: - raise werkzeug.exceptions.UnsupportedMediaType(f"Invalid content-type: {request.mimetype}! Supported types: " - + ", ".join(valid_content_types)) - - if request.mimetype == "application/json": - json_data = request.get_data() - try: - rv = json.loads(json_data, cls=StrippedAASFromJsonDecoder) - except json.decoder.JSONDecodeError as e: - raise BadRequest(str(e)) from e - else: - xml_data = io.BytesIO(request.get_data()) - rv = read_aas_xml_element(xml_data, xml_constructors[expect_type], stripped=True) - - assert isinstance(rv, expect_type) - return rv - - -def identifier_uri_encode(id_: model.Identifier) -> str: - # TODO: replace urllib with urllib3 if we're using it anyways? - return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") - - -def identifier_uri_decode(id_str: str) -> model.Identifier: - try: - id_type_str, id_ = id_str.split(":", 1) - except ValueError as e: - raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") - id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) - if id_type is None: - raise ValueError(f"Identifier Type '{id_type_str}' is invalid") - return model.Identifier(urllib.parse.unquote(id_), id_type) - - -class IdentifierConverter(werkzeug.routing.UnicodeConverter): - def to_url(self, value: model.Identifier) -> str: - return super().to_url(identifier_uri_encode(value)) - - def to_python(self, value: str) -> model.Identifier: - try: - return identifier_uri_decode(super().to_python(value)) - except ValueError as e: - raise BadRequest(str(e)) - - -class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore): - self.object_store: model.AbstractObjectStore = object_store - self.url_map = werkzeug.routing.Map([ - Submount("/api/v1", [ - Rule("/aas/", endpoint=self.get_aas), - Submount("/aas/", [ - Rule("/asset", methods=["GET"], endpoint=self.get_aas_asset), - Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodels), - Rule("/submodels", methods=["PUT"], endpoint=self.put_aas_submodels), - Rule("/views", methods=["GET"], endpoint=self.get_aas_views), - Rule("/views/", methods=["GET"], - endpoint=self.get_aas_views_specific), - Rule("/views/", methods=["DELETE"], - endpoint=self.delete_aas_views_specific), - Rule("/conceptDictionaries", methods=["GET"], endpoint=self.get_aas_concept_dictionaries), - Rule("/conceptDictionaries", methods=["PUT"], endpoint=self.put_aas_concept_dictionaries), - Rule("/conceptDictionaries/", methods=["GET"], - endpoint=self.get_aas_concept_dictionaries_specific), - Rule("/conceptDictionaries/", methods=["DELETE"], - endpoint=self.delete_aas_concept_dictionaries_specific), - Rule("/submodels/", methods=["GET"], - endpoint=self.get_aas_submodels_specific), - Rule("/submodels/", methods=["DELETE"], - endpoint=self.delete_aas_submodels_specific), - ]), - Rule("/submodels/", endpoint=self.get_submodel), - Submount("/submodels/", [ - Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/submodelElements", methods=["PUT"], endpoint=self.put_submodel_submodel_elements), - Rule("/submodelElements/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_specific_nested), - Rule("/submodelElements/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_specific) - ]) - ]) - ], converters={"identifier": IdentifierConverter}) - - def __call__(self, environ, start_response): - response = self.handle_request(Request(environ)) - return response(environ, start_response) - - def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: - identifiable = self.object_store.get(identifier) - if not isinstance(identifiable, type_): - raise NotFound(f"No {type_.__name__} with {identifier} found!") - return identifiable - - def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: - try: - return reference.resolve(self.object_store) - except (KeyError, TypeError, model.UnexpectedTypeError) as e: - raise InternalServerError(str(e)) from e - - def handle_request(self, request: Request): - adapter = self.url_map.bind_to_environ(request.environ) - try: - endpoint, values = adapter.match() - if endpoint is None: - raise NotImplemented("This route is not yet implemented.") - return endpoint(request, values) - # any raised error that leaves this function will cause a 500 internal server error - # so catch raised http exceptions and return them - except werkzeug.exceptions.NotAcceptable as e: - return e - except werkzeug.exceptions.HTTPException as e: - try: - # get_response_type() may raise a NotAcceptable error, so we have to handle that - return http_exception_to_response(e, get_response_type(request)) - except werkzeug.exceptions.NotAcceptable as e: - return e - - # --------- AAS ROUTES --------- - def get_aas(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - return response_t(Result(aas)) - - def get_aas_asset(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - asset = self._resolve_reference(aas.asset) - asset.update() - return response_t(Result(asset)) - - def get_aas_submodels(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - submodels = [self._resolve_reference(ref) for ref in aas.submodel] - for submodel in submodels: - submodel.update() - identification_id: Optional[str] = request.args.get("identification.id") - if identification_id is not None: - # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 - submodels = filter(lambda s: identification_id in s.identification.id, submodels) # type: ignore - semantic_id: Optional[str] = request.args.get("semanticId") - if semantic_id is not None: - # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 - submodels = filter(lambda s: s.semantic_id is not None # type: ignore - and len(s.semantic_id.key) > 0 - and semantic_id in s.semantic_id.key[0].value, submodels) # type: ignore - submodels_list = list(submodels) - if len(submodels_list) == 0: - raise NotFound("No submodels found!") - return response_t(Result(submodels_list)) - - def put_aas_submodels(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - new_submodel = parse_request_body(request, model.Submodel) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - current_submodel = None - for s in iter(self._resolve_reference(ref) for ref in aas.submodel): - if s.identification == new_submodel.identification: - current_submodel = s - break - if current_submodel is None: - aas.submodel.add(model.AASReference.from_referable(new_submodel)) - aas.commit() - not_referenced_submodel = self.object_store.get(new_submodel.identification) - assert(isinstance(not_referenced_submodel, model.Submodel)) - current_submodel = not_referenced_submodel - if current_submodel is not None: - self.object_store.discard(current_submodel) - self.object_store.add(new_submodel) - return response_t(Result(new_submodel), status=201) - - def get_aas_views(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - if len(aas.view) == 0: - raise NotFound("No views found!") - return response_t(Result((aas.view,))) - - def put_aas_views(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - new_view = parse_request_body(request, model.View) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - old_view = aas.view.get(new_view.id_short) - if old_view is not None: - aas.view.discard(old_view) - aas.view.add(new_view) - aas.commit() - return response_t(Result(new_view), status=201) - - def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - view = aas.view.get(id_short) - if view is None: - raise NotFound(f"No view with idShort '{id_short}' found!") - view.update() - return response_t(Result(view)) - - def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - view = aas.view.get(id_short) - if view is None: - raise NotFound(f"No view with idShort '{id_short}' found!") - view.update() - aas.view.remove(view.id_short) - return Response(status=204) - - def get_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - if len(aas.concept_dictionary) == 0: - raise NotFound("No concept dictionaries found!") - return response_t(Result((aas.concept_dictionary,))) - - def put_aas_concept_dictionaries(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - new_concept_dictionary = parse_request_body(request, model.ConceptDictionary) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - old_concept_dictionary = aas.concept_dictionary.get(new_concept_dictionary.id_short) - if old_concept_dictionary is not None: - aas.concept_dictionary.discard(old_concept_dictionary) - aas.concept_dictionary.add(new_concept_dictionary) - aas.commit() - return response_t(Result((new_concept_dictionary,)), status=201) - - def get_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - concept_dictionary = aas.concept_dictionary.get(id_short) - if concept_dictionary is None: - raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") - concept_dictionary.update() - return response_t(Result(concept_dictionary)) - - def delete_aas_concept_dictionaries_specific(self, request: Request, url_args: Dict) -> Response: - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - concept_dictionary = aas.concept_dictionary.get(id_short) - if concept_dictionary is None: - raise NotFound(f"No concept dictionary with idShort '{id_short}' found!") - concept_dictionary.update() - aas.view.remove(concept_dictionary.id_short) - return Response(status=204) - - def get_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - for submodel in iter(self._resolve_reference(ref) for ref in aas.submodel): - submodel.update() - if submodel.id_short == id_short: - return response_t(Result(submodel)) - raise NotFound(f"No submodel with idShort '{id_short}' found!") - - def delete_aas_submodels_specific(self, request: Request, url_args: Dict) -> Response: - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - id_short = url_args["id_short"] - for ref in aas.submodel: - submodel = self._resolve_reference(ref) - submodel.update() - if submodel.id_short == id_short: - aas.submodel.discard(ref) - self.object_store.discard(submodel) - return Response(status=204) - raise NotFound(f"No submodel with idShort '{id_short}' found!") - - # --------- SUBMODEL ROUTES --------- - def get_submodel(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - return response_t(Result(submodel)) - - def get_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_elements = submodel.submodel_element - semantic_id: Optional[str] = request.args.get("semanticId") - if semantic_id is not None: - # mypy doesn't propagate type restrictions to nested functions: https://github.com/python/mypy/issues/2608 - submodel_elements = filter(lambda se: se.semantic_id is not None # type: ignore - and len(se.semantic_id.key) > 0 - and semantic_id in se.semantic_id.key[0].value, submodel_elements # type: ignore - ) - submodel_elements_tuple = (submodel_elements,) - if len(submodel_elements_tuple) == 0: - raise NotFound("No submodel elements found!") - return response_t(Result(submodel_elements_tuple)) - - def put_submodel_submodel_elements(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - new_concept_dictionary = parse_request_body(request, model.SubmodelElement) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - old_submodel_element = submodel.submodel_element.get(new_concept_dictionary.id_short) - if old_submodel_element is not None: - submodel.submodel_element.discard(old_submodel_element) - submodel.submodel_element.add(new_concept_dictionary) - submodel.commit() - return response_t(Result(new_concept_dictionary), status=201) - - def get_submodel_submodel_element_specific_nested(self, request: Request, url_args: Dict) -> Response: - response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - id_shorts: List[str] = url_args["id_shorts"].split("/") - submodel_element: model.SubmodelElement = \ - model.SubmodelElementCollectionUnordered("init_wrapper", submodel.submodel_element) - for id_short in id_shorts: - if not isinstance(submodel_element, model.SubmodelElementCollection): - raise NotFound(f"Nested submodel element {submodel_element} is not a submodel element collection!") - try: - submodel_element = submodel_element.value.get_referable(id_short) - except KeyError: - raise NotFound(f"No nested submodel element with idShort '{id_short}' found!") - submodel_element.update() - return response_t(Result(submodel_element)) - - def delete_submodel_submodel_element_specific(self, request: Request, url_args: Dict) -> Response: - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - id_short = url_args["id_short"] - submodel_element = submodel.submodel_element.get(id_short) - if submodel_element is None: - raise NotFound(f"No submodel element with idShort '{id_short}' found!") - submodel_element.update() - submodel.submodel_element.remove(submodel_element.id_short) - return Response(status=204) From 8f50d8e418ebc0c70f148a8fc09453e46ad6defd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 19 Nov 2020 03:23:24 +0100 Subject: [PATCH 011/157] adapter.http: add aas.view routes --- basyx/aas/adapter/http.py | 62 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 1b5fe1bdd..cf545f53c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -290,6 +290,14 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_aas_submodel_refs_specific), Rule("/submodels/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific), + Rule("/views", methods=["GET"], endpoint=self.get_aas_views), + Rule("/views", methods=["POST"], endpoint=self.post_aas_views), + Rule("/views/", methods=["GET"], + endpoint=self.get_aas_views_specific), + Rule("/views/", methods=["PUT"], + endpoint=self.put_aas_views_specific), + Rule("/views/", methods=["DELETE"], + endpoint=self.delete_aas_views_specific) ]), Rule("/submodels/", endpoint=self.get_submodel), ]) @@ -384,6 +392,60 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> aas.commit() return response_t(Result(None)) + def get_aas_views(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + return response_t(Result(tuple(aas.view))) + + def post_aas_views(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + view = parse_request_body(request, model.View) + if view.id_short in aas.view: + raise Conflict(f"View with idShort {view.id_short} already exists!") + aas.view.add(view) + aas.commit() + return response_t(Result(view)) + + def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + view_idshort = url_args["view_idshort"] + view = aas.view.get(view_idshort) + if view is None: + raise NotFound(f"No view with idShort {view_idshort} found!") + # TODO: is view.update() necessary here? + view.update() + return response_t(Result(view)) + + def put_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + view_idshort = url_args["view_idshort"] + view = aas.view.get(view_idshort) + if view is None: + raise NotFound(f"No view with idShort {view_idshort} found!") + new_view = parse_request_body(request, model.View) + if new_view.id_short != view.id_short: + raise BadRequest(f"idShort of new {new_view} doesn't match the old {view}") + aas.view.remove(view) + aas.view.add(new_view) + return response_t(Result(new_view)) + + def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + view_idshort = url_args["view_idshort"] + if view_idshort not in aas.view: + raise NotFound(f"No view with idShort {view_idshort} found!") + aas.view.remove(view_idshort) + return response_t(Result(None)) + # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict) -> Response: response_t = get_response_type(request) From 6dd915374a7570232a85e8b767064008cc091de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:24:00 +0100 Subject: [PATCH 012/157] adapter.http: remove unused imports --- basyx/aas/adapter/http.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index cf545f53c..467672fb4 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -16,7 +16,6 @@ import json from lxml import etree # type: ignore import urllib.parse -import werkzeug from werkzeug.exceptions import BadRequest, Conflict, InternalServerError, NotFound, NotImplemented from werkzeug.routing import Rule, Submount from werkzeug.wrappers import Request, Response @@ -26,7 +25,7 @@ from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Dict, Iterable, List, Optional, Tuple, Type, Union +from typing import Dict, Iterable, Optional, Tuple, Type, Union @enum.unique From 6cdf0300844a02a266acfee690eeae8a6320d225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:25:29 +0100 Subject: [PATCH 013/157] adapter.http: use werkzeug instead of urllib to quote and unquote identifiers --- basyx/aas/adapter/http.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 467672fb4..42829463c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -15,7 +15,7 @@ import io import json from lxml import etree # type: ignore -import urllib.parse +import werkzeug.urls from werkzeug.exceptions import BadRequest, Conflict, InternalServerError, NotFound, NotImplemented from werkzeug.routing import Rule, Submount from werkzeug.wrappers import Request, Response @@ -250,8 +250,7 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m def identifier_uri_encode(id_: model.Identifier) -> str: - # TODO: replace urllib with urllib3 if we're using it anyways? - return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") + return IDENTIFIER_TYPES[id_.id_type] + ":" + werkzeug.urls.url_quote(id_.id, safe="") def identifier_uri_decode(id_str: str) -> model.Identifier: @@ -261,8 +260,8 @@ def identifier_uri_decode(id_str: str) -> model.Identifier: raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) if id_type is None: - raise ValueError(f"Identifier Type '{id_type_str}' is invalid") - return model.Identifier(urllib.parse.unquote(id_), id_type) + raise ValueError(f"IdentifierType '{id_type_str}' is invalid") + return model.Identifier(werkzeug.urls.url_unquote(id_), id_type) class IdentifierConverter(werkzeug.routing.UnicodeConverter): From 88667066b1e9e532c80beb0127de89a8865a4082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:29:01 +0100 Subject: [PATCH 014/157] adapter.http: add dirty hack to deserialize json references --- basyx/aas/adapter/http.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 42829463c..3403df754 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -239,6 +239,10 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m try: if request.mimetype == "application/json": rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) + # TODO: the following is ugly, but necessary because references aren't self-identified objects + # in the json schema + if expect_type is model.AASReference: + rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) else: xml_data = io.BytesIO(request.get_data()) rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) From be57ecf53d30b0c806231c4f405a74a8b181b80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:35:17 +0100 Subject: [PATCH 015/157] adapter.http: add location header to 201 responses using the werkzeug.routing.MapAdapter url builder --- basyx/aas/adapter/http.py | 52 +++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 3403df754..b88169973 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -331,12 +331,12 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi raise NotFound(f"No reference to submodel with {sm_identifier} found!") def handle_request(self, request: Request): - adapter = self.url_map.bind_to_environ(request.environ) + map_adapter = self.url_map.bind_to_environ(request.environ) try: - endpoint, values = adapter.match() + endpoint, values = map_adapter.match() if endpoint is None: raise NotImplemented("This route is not yet implemented.") - return endpoint(request, values) + return endpoint(request, values, map_adapter=map_adapter) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them except werkzeug.exceptions.NotAcceptable as e: @@ -349,39 +349,50 @@ def handle_request(self, request: Request): return e # --------- AAS ROUTES --------- - def get_aas(self, request: Request, url_args: Dict) -> Response: + def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() return response_t(Result(aas)) - def get_aas_submodel_refs(self, request: Request, url_args: Dict) -> Response: + def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() return response_t(Result(tuple(aas.submodel))) - def post_aas_submodel_refs(self, request: Request, url_args: Dict) -> Response: + def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: werkzeug.routing.MapAdapter) \ + -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas_identifier = url_args["aas_id"] + aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() sm_ref = parse_request_body(request, model.AASReference) # type: ignore assert isinstance(sm_ref, model.AASReference) + # to give a location header in the response we have to be able to get the submodel identifier from the reference + try: + submodel_identifier = sm_ref.get_identifier() + except ValueError as e: + raise BadRequest(f"Can't resolve submodel identifier for given reference!") from e if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") # TODO: check if reference references a non-existant submodel? aas.submodel.add(sm_ref) aas.commit() - return response_t(Result(sm_ref), status=201) + created_resource_url = map_adapter.build(self.get_aas_submodel_refs_specific, { + "aas_id": aas_identifier, + "sm_id": submodel_identifier + }, force_external=True) + return response_t(Result(sm_ref), status=201, headers={"Location": created_resource_url}) - def get_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> Response: + def get_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) return response_t(Result(sm_ref)) - def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> Response: + def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() @@ -394,24 +405,29 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict) -> aas.commit() return response_t(Result(None)) - def get_aas_views(self, request: Request, url_args: Dict) -> Response: + def get_aas_views(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() return response_t(Result(tuple(aas.view))) - def post_aas_views(self, request: Request, url_args: Dict) -> Response: + def post_aas_views(self, request: Request, url_args: Dict, map_adapter: werkzeug.routing.MapAdapter) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas_identifier = url_args["aas_id"] + aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() view = parse_request_body(request, model.View) if view.id_short in aas.view: raise Conflict(f"View with idShort {view.id_short} already exists!") aas.view.add(view) aas.commit() - return response_t(Result(view)) + created_resource_url = map_adapter.build(self.get_aas_views_specific, { + "aas_id": aas_identifier, + "view_idshort": view.id_short + }, force_external=True) + return response_t(Result(view), status=201, headers={"Location": created_resource_url}) - def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() @@ -423,7 +439,7 @@ def get_aas_views_specific(self, request: Request, url_args: Dict) -> Response: view.update() return response_t(Result(view)) - def put_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() @@ -438,7 +454,7 @@ def put_aas_views_specific(self, request: Request, url_args: Dict) -> Response: aas.view.add(new_view) return response_t(Result(new_view)) - def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Response: + def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() @@ -449,7 +465,7 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict) -> Respons return response_t(Result(None)) # --------- SUBMODEL ROUTES --------- - def get_submodel(self, request: Request, url_args: Dict) -> Response: + def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() From a414d67cc60aa16de0b8343167632ecc3aaf5a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 20 Nov 2020 03:36:17 +0100 Subject: [PATCH 016/157] adapter.http: raise BadRequest from ValueError in IdentifierConverter --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index b88169973..f29f61424 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -276,7 +276,7 @@ def to_python(self, value: str) -> model.Identifier: try: return identifier_uri_decode(super().to_python(value)) except ValueError as e: - raise BadRequest(str(e)) + raise BadRequest(str(e)) from e class WSGIApp: From ef9664016d0e1af6c6ee0ca4c08f58e6e5060edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 23 Nov 2020 18:23:53 +0100 Subject: [PATCH 017/157] adapter.http: check type of POST'ed references --- basyx/aas/adapter/http.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index f29f61424..94aa7fd1f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -241,6 +241,8 @@ def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> m rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema + # TODO: json deserialization automatically creates AASReference[Submodel] this way, so we can't check, + # whether the client posted a submodel reference or not if expect_type is model.AASReference: rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) else: @@ -369,6 +371,8 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: aas.update() sm_ref = parse_request_body(request, model.AASReference) # type: ignore assert isinstance(sm_ref, model.AASReference) + if sm_ref.type is not model.Submodel: + raise BadRequest(f"{sm_ref!r} does not reference a Submodel!") # to give a location header in the response we have to be able to get the submodel identifier from the reference try: submodel_identifier = sm_ref.get_identifier() From 76a444c7000f5dc8c5aee3fadbb054c04eea68cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Nov 2020 21:20:13 +0100 Subject: [PATCH 018/157] adapter.http: refactor imports --- basyx/aas/adapter/http.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 94aa7fd1f..09e8bf940 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -15,9 +15,11 @@ import io import json from lxml import etree # type: ignore +import werkzeug.exceptions +import werkzeug.routing import werkzeug.urls -from werkzeug.exceptions import BadRequest, Conflict, InternalServerError, NotFound, NotImplemented -from werkzeug.routing import Rule, Submount +from werkzeug.exceptions import BadRequest, Conflict, NotFound +from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response from aas import model @@ -91,7 +93,7 @@ def default(self, obj: object) -> object: return super().default(obj) -class APIResponse(abc.ABC, werkzeug.wrappers.Response): +class APIResponse(abc.ABC, Response): @abc.abstractmethod def __init__(self, result: Result, *args, **kwargs): super().__init__(*args, **kwargs) @@ -321,7 +323,7 @@ def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> m try: return reference.resolve(self.object_store) except (KeyError, TypeError, model.UnexpectedTypeError) as e: - raise InternalServerError(str(e)) from e + raise werkzeug.exceptions.InternalServerError(str(e)) from e @classmethod def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdministrationShell, @@ -333,11 +335,11 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi raise NotFound(f"No reference to submodel with {sm_identifier} found!") def handle_request(self, request: Request): - map_adapter = self.url_map.bind_to_environ(request.environ) + map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = map_adapter.match() if endpoint is None: - raise NotImplemented("This route is not yet implemented.") + raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") return endpoint(request, values, map_adapter=map_adapter) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them @@ -363,8 +365,7 @@ def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas.update() return response_t(Result(tuple(aas.submodel))) - def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: werkzeug.routing.MapAdapter) \ - -> Response: + def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) @@ -415,7 +416,7 @@ def get_aas_views(self, request: Request, url_args: Dict, **_kwargs) -> Response aas.update() return response_t(Result(tuple(aas.view))) - def post_aas_views(self, request: Request, url_args: Dict, map_adapter: werkzeug.routing.MapAdapter) -> Response: + def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) From 9d27d12e304480bcc895dd1bd12580f7907a4a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Nov 2020 21:24:11 +0100 Subject: [PATCH 019/157] adapter.http: refactor routing Requests to URIs with a trailing slash will now be redirected to the respective URI without trailing slash. e.g. GET /aas/$identifier/ -> GET /aas/$identifier A redirect will only be set if the respective URI without trailing slash exists and the current request method is valid for the new URI. Historically, the trailing slash was only present when the requested resource was a directory. In our case the resources don't work like directories, in the sense, that each resource doesn't even list possible subsequent resources. So because our resources don't behave like directories, they shouldn't have a trailing slash. --- basyx/aas/adapter/http.py | 52 ++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 09e8bf940..8655fe38b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -288,26 +288,32 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - Rule("/aas/", endpoint=self.get_aas), Submount("/aas/", [ - Rule("/submodels", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/submodels", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("/submodels/", methods=["GET"], - endpoint=self.get_aas_submodel_refs_specific), - Rule("/submodels/", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific), - Rule("/views", methods=["GET"], endpoint=self.get_aas_views), - Rule("/views", methods=["POST"], endpoint=self.post_aas_views), - Rule("/views/", methods=["GET"], - endpoint=self.get_aas_views_specific), - Rule("/views/", methods=["PUT"], - endpoint=self.put_aas_views_specific), - Rule("/views/", methods=["DELETE"], - endpoint=self.delete_aas_views_specific) + Rule("/", methods=["GET"], endpoint=self.get_aas), + Submount("/submodels", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("/", methods=["GET"], + endpoint=self.get_aas_submodel_refs_specific), + Rule("/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific) + ]), + Submount("/views", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_views), + Rule("/", methods=["POST"], endpoint=self.post_aas_views), + Rule("/", methods=["GET"], + endpoint=self.get_aas_views_specific), + Rule("/", methods=["PUT"], + endpoint=self.put_aas_views_specific), + Rule("/", methods=["DELETE"], + endpoint=self.delete_aas_views_specific) + ]) ]), - Rule("/submodels/", endpoint=self.get_submodel), + Submount("/submodels/", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel), + ]) ]) - ], converters={"identifier": IdentifierConverter}) + ], converters={"identifier": IdentifierConverter}, strict_slashes=False) def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) @@ -337,6 +343,18 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: + # redirect requests with a trailing slash to the path without trailing slash + # if the path without trailing slash exists. + # if not, map_adapter.match() will raise NotFound() in both cases + if request.path != "/" and request.path.endswith("/"): + map_adapter.match(request.path[:-1], request.method) + # from werkzeug's internal routing redirection + raise werkzeug.routing.RequestRedirect( + map_adapter.make_redirect_url( + werkzeug.urls.url_quote(request.path[:-1], map_adapter.map.charset, safe="/:|+"), + map_adapter.query_args + ) + ) endpoint, values = map_adapter.match() if endpoint is None: raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") From fc2d747ef30ac741c2c5705e662617ee878f5b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Nov 2020 21:36:12 +0100 Subject: [PATCH 020/157] adapter.http fix type annotations of parse_request_body() --- basyx/aas/adapter/http.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 8655fe38b..efcdd175d 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -27,7 +27,7 @@ from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Dict, Iterable, Optional, Tuple, Type, Union +from typing import Dict, Iterable, Optional, Tuple, Type, TypeVar, Union @enum.unique @@ -216,7 +216,10 @@ def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, res return response_type(result, status=exception.code, headers=headers) -def parse_request_body(request: Request, expect_type: Type[model.base._RT]) -> model.base._RT: +T = TypeVar("T") + + +def parse_request_body(request: Request, expect_type: Type[T]) -> T: """ TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent running out of memory. but it doesn't state how to check the content length @@ -388,8 +391,7 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - sm_ref = parse_request_body(request, model.AASReference) # type: ignore - assert isinstance(sm_ref, model.AASReference) + sm_ref = parse_request_body(request, model.AASReference) if sm_ref.type is not model.Submodel: raise BadRequest(f"{sm_ref!r} does not reference a Submodel!") # to give a location header in the response we have to be able to get the submodel identifier from the reference From 3ed487d1afa44f06b90464de0693549c52664954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 24 Nov 2020 21:38:05 +0100 Subject: [PATCH 021/157] adapter.http: remove whitespaces from json responses --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index efcdd175d..7dc11ec21 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -109,7 +109,7 @@ def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) def serialize(self, result: Result) -> str: - return json.dumps(result, cls=ResultToJsonEncoder) + return json.dumps(result, cls=ResultToJsonEncoder, separators=(",", ":")) class XmlResponse(APIResponse): From 40b317659ad0fd97aabba5527c7fddaee647f2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 25 Nov 2020 19:51:47 +0100 Subject: [PATCH 022/157] adapter.http: cleanup TODO's an remove unnecessary checks --- basyx/aas/adapter/http.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7dc11ec21..8525ff5e7 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -224,6 +224,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent running out of memory. but it doesn't state how to check the content length also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema + In the meeting (25.11.2020) we discussed, this may refer to a reverse proxy in front of this WSGI app, + which should limit the maximum content length. """ type_constructables_map = { model.AASReference: XMLConstructables.AAS_REFERENCE, @@ -246,8 +248,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema - # TODO: json deserialization automatically creates AASReference[Submodel] this way, so we can't check, - # whether the client posted a submodel reference or not + # TODO: json deserialization will always create an AASReference[Submodel], xml deserialization determines + # that automatically if expect_type is model.AASReference: rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) else: @@ -392,8 +394,6 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() sm_ref = parse_request_body(request, model.AASReference) - if sm_ref.type is not model.Submodel: - raise BadRequest(f"{sm_ref!r} does not reference a Submodel!") # to give a location header in the response we have to be able to get the submodel identifier from the reference try: submodel_identifier = sm_ref.get_identifier() @@ -401,7 +401,6 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: raise BadRequest(f"Can't resolve submodel identifier for given reference!") from e if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") - # TODO: check if reference references a non-existant submodel? aas.submodel.add(sm_ref) aas.commit() created_resource_url = map_adapter.build(self.get_aas_submodel_refs_specific, { @@ -460,8 +459,6 @@ def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> view = aas.view.get(view_idshort) if view is None: raise NotFound(f"No view with idShort {view_idshort} found!") - # TODO: is view.update() necessary here? - view.update() return response_t(Result(view)) def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: From f13f0f95eff9f6f4b7eb383e1630390e00e7f7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 25 Nov 2020 20:15:34 +0100 Subject: [PATCH 023/157] adapter.http: run debug app on example_aas_missing_attributes --- basyx/aas/adapter/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 8525ff5e7..76fb6197b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -496,5 +496,6 @@ def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: if __name__ == "__main__": from werkzeug.serving import run_simple - from aas.examples.data.example_aas import create_full_example + # use example_aas_missing_attributes, because the AAS from example_aas has no views + from aas.examples.data.example_aas_missing_attributes import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From 88e923e41d1d876be9159b97a46f27db2275b7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 3 Dec 2020 17:21:28 +0100 Subject: [PATCH 024/157] adapter.http: get root cause in parse_request_body() for better error messages --- basyx/aas/adapter/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 76fb6197b..0e4bfd064 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -256,6 +256,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: xml_data = io.BytesIO(request.get_data()) rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError) as e: + while e.__cause__ is not None: + e = e.__cause__ raise BadRequest(str(e)) from e assert isinstance(rv, expect_type) From 1760ae13ce3d7e73cfdd105b2214add0249e41bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 3 Dec 2020 17:22:40 +0100 Subject: [PATCH 025/157] adapter.http: remove minlength=1 from string url parameters because that's default anyways --- basyx/aas/adapter/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 0e4bfd064..d60c29174 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -308,11 +308,11 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/views", [ Rule("/", methods=["GET"], endpoint=self.get_aas_views), Rule("/", methods=["POST"], endpoint=self.post_aas_views), - Rule("/", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.get_aas_views_specific), - Rule("/", methods=["PUT"], + Rule("/", methods=["PUT"], endpoint=self.put_aas_views_specific), - Rule("/", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_aas_views_specific) ]) ]), From 19ab6e02a6a75b3e0dae38c333b468fd024bd235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 3 Dec 2020 17:23:55 +0100 Subject: [PATCH 026/157] adapter.http: rewrite idShort as id_short in error message to stay consistent with the rest of this library --- basyx/aas/adapter/http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d60c29174..d3cf63d86 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -444,7 +444,7 @@ def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapt aas.update() view = parse_request_body(request, model.View) if view.id_short in aas.view: - raise Conflict(f"View with idShort {view.id_short} already exists!") + raise Conflict(f"View with id_short {view.id_short} already exists!") aas.view.add(view) aas.commit() created_resource_url = map_adapter.build(self.get_aas_views_specific, { @@ -460,7 +460,7 @@ def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> view_idshort = url_args["view_idshort"] view = aas.view.get(view_idshort) if view is None: - raise NotFound(f"No view with idShort {view_idshort} found!") + raise NotFound(f"No view with id_short {view_idshort} found!") return response_t(Result(view)) def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -470,10 +470,10 @@ def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> view_idshort = url_args["view_idshort"] view = aas.view.get(view_idshort) if view is None: - raise NotFound(f"No view with idShort {view_idshort} found!") + raise NotFound(f"No view with id_short {view_idshort} found!") new_view = parse_request_body(request, model.View) if new_view.id_short != view.id_short: - raise BadRequest(f"idShort of new {new_view} doesn't match the old {view}") + raise BadRequest(f"id_short of new {new_view} doesn't match the old {view}") aas.view.remove(view) aas.view.add(new_view) return response_t(Result(new_view)) @@ -484,7 +484,7 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) aas.update() view_idshort = url_args["view_idshort"] if view_idshort not in aas.view: - raise NotFound(f"No view with idShort {view_idshort} found!") + raise NotFound(f"No view with id_short {view_idshort} found!") aas.view.remove(view_idshort) return response_t(Result(None)) From 29ca324862cd11fd3fe50b15b15e72a51cfc5a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 3 Dec 2020 17:24:59 +0100 Subject: [PATCH 027/157] adapter.http: add nested submodel elements endpoints add IdShortPathConverter and helper functions --- basyx/aas/adapter/http.py | 135 +++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d3cf63d86..522d8d177 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -27,7 +27,7 @@ from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Dict, Iterable, Optional, Tuple, Type, TypeVar, Union +from typing import Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union @enum.unique @@ -290,6 +290,35 @@ def to_python(self, value: str) -> model.Identifier: raise BadRequest(str(e)) from e +def validate_id_short(id_short: str) -> bool: + try: + model.MultiLanguageProperty(id_short) + except ValueError: + return False + return True + + +class IdShortPathConverter(werkzeug.routing.PathConverter): + id_short_prefix = "!" + + def to_url(self, value: List[str]) -> str: + for id_short in value: + if not validate_id_short(id_short): + raise ValueError(f"{id_short} is not a valid id_short!") + return "/".join([self.id_short_prefix + id_short for id_short in value]) + + def to_python(self, value: str) -> List[str]: + id_shorts = super().to_python(value).split("/") + for idx, id_short in enumerate(id_shorts): + if not id_short.startswith(self.id_short_prefix): + raise werkzeug.routing.ValidationError + id_short = id_short[1:] + if not validate_id_short(id_short): + raise BadRequest(f"{id_short} is not a valid id_short!") + id_shorts[idx] = id_short + return id_shorts + + class WSGIApp: def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store @@ -318,9 +347,20 @@ def __init__(self, object_store: model.AbstractObjectStore): ]), Submount("/submodels/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), + Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodelElements", methods=["POST"], endpoint=self.post_submodel_submodel_elements), + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_specific_nested), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_specific_nested), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_specific_nested) ]) ]) - ], converters={"identifier": IdentifierConverter}, strict_slashes=False) + ], converters={ + "identifier": IdentifierConverter, + "id_short_path": IdShortPathConverter + }, strict_slashes=False) def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) @@ -347,6 +387,33 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi return sm_ref raise NotFound(f"No reference to submodel with {sm_identifier} found!") + @classmethod + def _get_nested_submodel_element(cls, namespace: model.Namespace, id_shorts: List[str]) -> model.SubmodelElement: + current_namespace: Union[model.Namespace, model.SubmodelElement] = namespace + for id_short in id_shorts: + current_namespace = cls._expect_namespace(current_namespace, id_short) + next_obj = cls._namespace_submodel_element_op(current_namespace, current_namespace.get_referable, id_short) + if not isinstance(next_obj, model.SubmodelElement): + raise werkzeug.exceptions.InternalServerError(f"{next_obj}, child of {current_namespace!r}, " + f"is not a submodel element!") + current_namespace = next_obj + if not isinstance(current_namespace, model.SubmodelElement): + raise TypeError("No id_shorts specified!") + return current_namespace + + @classmethod + def _expect_namespace(cls, obj: object, needle: str) -> model.Namespace: + if not isinstance(obj, model.Namespace): + raise BadRequest(f"{obj!r} is not a namespace, can't locate {needle}!") + return obj + + @classmethod + def _namespace_submodel_element_op(cls, namespace: model.Namespace, op: Callable[[str], T], arg: str) -> T: + try: + return op(arg) + except KeyError as e: + raise NotFound(f"Submodel element with id_short {arg} not found in {namespace!r}") from e + def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: @@ -495,6 +562,70 @@ def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: submodel.update() return response_t(Result(submodel)) + def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(Result(tuple(submodel.submodel_element))) + + def post_submodel_submodel_elements(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore + if submodel_element.id_short in submodel.submodel_element: + raise Conflict(f"Submodel element with id_short {submodel_element.id_short} already exists!") + submodel.submodel_element.add(submodel_element) + submodel.commit() + created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_specific_nested, { + "submodel_id": submodel_identifier, + "id_shorts": [submodel_element.id_short] + }, force_external=True) + return response_t(Result(submodel_element), status=201, headers={"Location": created_resource_url}) + + def get_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + return response_t(Result(submodel_element)) + + def put_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + new_submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore + mismatch_error_message = f" of new submodel element {new_submodel_element} doesn't not match " \ + f"the current submodel element {submodel_element}" + if not type(submodel_element) is type(new_submodel_element): + raise BadRequest("Type" + mismatch_error_message) + if submodel_element.id_short != new_submodel_element.id_short: + raise BadRequest("id_short" + mismatch_error_message) + submodel_element.update_from(new_submodel_element) + submodel_element.commit() + return response_t(Result(submodel_element)) + + def delete_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + id_shorts: List[str] = url_args["id_shorts"] + parent: model.Namespace = submodel + if len(id_shorts) > 1: + parent = self._expect_namespace( + self._get_nested_submodel_element(submodel, id_shorts[:-1]), + id_shorts[-1] + ) + self._namespace_submodel_element_op(parent, parent.remove_referable, id_shorts[-1]) + return response_t(Result(None)) + if __name__ == "__main__": from werkzeug.serving import run_simple From 779d15eeab93d8401242649ad802cdd930ce8917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 9 Dec 2020 19:38:55 +0100 Subject: [PATCH 028/157] adapter.http: compare id_shorts case-insensitively in PUT routes change type check from `not A is B` to `A is not B` for better readability --- basyx/aas/adapter/http.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 522d8d177..16bf535a3 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -539,7 +539,8 @@ def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> if view is None: raise NotFound(f"No view with id_short {view_idshort} found!") new_view = parse_request_body(request, model.View) - if new_view.id_short != view.id_short: + # compare id_shorts case-insensitively + if new_view.id_short.lower() != view.id_short.lower(): raise BadRequest(f"id_short of new {new_view} doesn't match the old {view}") aas.view.remove(view) aas.view.add(new_view) @@ -603,9 +604,10 @@ def put_submodel_submodel_elements_specific_nested(self, request: Request, url_a new_submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore mismatch_error_message = f" of new submodel element {new_submodel_element} doesn't not match " \ f"the current submodel element {submodel_element}" - if not type(submodel_element) is type(new_submodel_element): + if type(submodel_element) is not type(new_submodel_element): raise BadRequest("Type" + mismatch_error_message) - if submodel_element.id_short != new_submodel_element.id_short: + # compare id_shorts case-insensitively + if submodel_element.id_short.lower() != new_submodel_element.id_short.lower(): raise BadRequest("id_short" + mismatch_error_message) submodel_element.update_from(new_submodel_element) submodel_element.commit() From 000e4b39b5ecde5adc46828032cd7d51441f0a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 9 Dec 2020 19:42:48 +0100 Subject: [PATCH 029/157] adapter.http: add nested submodel element endpoints for type-specific attributes like statement, annotation and value --- basyx/aas/adapter/http.py | 59 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 16bf535a3..e0283c581 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -354,7 +354,26 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_elements_specific_nested), Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_specific_nested) + endpoint=self.delete_submodel_submodel_elements_specific_nested), + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + Rule("//values", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("//values", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("//annotations", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation")), + Rule("//annotations", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation", + request_body_type=model.DataElement)), # type: ignore + Rule("//statements", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, "statement")), + Rule("//statements", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, "statement")) ]) ]) ], converters={ @@ -628,6 +647,44 @@ def delete_submodel_submodel_elements_specific_nested(self, request: Request, ur self._namespace_submodel_element_op(parent, parent.remove_referable, id_shorts[-1]) return response_t(Result(None)) + # --------- SUBMODEL ROUTE FACTORIES --------- + def factory_get_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str) \ + -> Callable[[Request, Dict], Response]: + def route(request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + if not isinstance(submodel_element, type_): + raise BadRequest(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") + return response_t(Result(tuple(getattr(submodel_element, attr)))) + return route + + def factory_post_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str, + request_body_type: Type[model.SubmodelElement] + = model.SubmodelElement) \ + -> Callable[[Request, Dict, MapAdapter], Response]: + def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + id_shorts = url_args["id_shorts"] + submodel_element = self._get_nested_submodel_element(submodel, id_shorts) + if not isinstance(submodel_element, type_): + raise BadRequest(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") + new_submodel_element = parse_request_body(request, request_body_type) + if new_submodel_element.id_short in getattr(submodel_element, attr): + raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") + getattr(submodel_element, attr).add(new_submodel_element) + submodel_element.commit() + created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_specific_nested, { + "submodel_id": submodel_identifier, + "id_shorts": id_shorts + [new_submodel_element.id_short] + }, force_external=True) + return response_t(Result(new_submodel_element), status=201, headers={"Location": created_resource_url}) + return route + if __name__ == "__main__": from werkzeug.serving import run_simple From 257ba143faa3d5fc7fa899ef38b42f5ebd66aeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 16 Feb 2021 22:09:48 +0100 Subject: [PATCH 030/157] adapter.http: update to V3.0RC01 --- basyx/aas/adapter/http.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e0283c581..93da59ac2 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -407,8 +407,9 @@ def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdmi raise NotFound(f"No reference to submodel with {sm_identifier} found!") @classmethod - def _get_nested_submodel_element(cls, namespace: model.Namespace, id_shorts: List[str]) -> model.SubmodelElement: - current_namespace: Union[model.Namespace, model.SubmodelElement] = namespace + def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, id_shorts: List[str]) \ + -> model.SubmodelElement: + current_namespace: Union[model.UniqueIdShortNamespace, model.SubmodelElement] = namespace for id_short in id_shorts: current_namespace = cls._expect_namespace(current_namespace, id_short) next_obj = cls._namespace_submodel_element_op(current_namespace, current_namespace.get_referable, id_short) @@ -421,13 +422,14 @@ def _get_nested_submodel_element(cls, namespace: model.Namespace, id_shorts: Lis return current_namespace @classmethod - def _expect_namespace(cls, obj: object, needle: str) -> model.Namespace: - if not isinstance(obj, model.Namespace): + def _expect_namespace(cls, obj: object, needle: str) -> model.UniqueIdShortNamespace: + if not isinstance(obj, model.UniqueIdShortNamespace): raise BadRequest(f"{obj!r} is not a namespace, can't locate {needle}!") return obj @classmethod - def _namespace_submodel_element_op(cls, namespace: model.Namespace, op: Callable[[str], T], arg: str) -> T: + def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, op: Callable[[str], T], arg: str) \ + -> T: try: return op(arg) except KeyError as e: @@ -544,7 +546,7 @@ def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] - view = aas.view.get(view_idshort) + view = aas.view.get("id_short", view_idshort) if view is None: raise NotFound(f"No view with id_short {view_idshort} found!") return response_t(Result(view)) @@ -554,7 +556,7 @@ def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] - view = aas.view.get(view_idshort) + view = aas.view.get("id_short", view_idshort) if view is None: raise NotFound(f"No view with id_short {view_idshort} found!") new_view = parse_request_body(request, model.View) @@ -638,7 +640,7 @@ def delete_submodel_submodel_elements_specific_nested(self, request: Request, ur submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() id_shorts: List[str] = url_args["id_shorts"] - parent: model.Namespace = submodel + parent: model.UniqueIdShortNamespace = submodel if len(id_shorts) > 1: parent = self._expect_namespace( self._get_nested_submodel_element(submodel, id_shorts[:-1]), From 4cb50026853a1769f4e95443a4d6a9b94a2c55e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:13:44 +0100 Subject: [PATCH 031/157] adapter.http: change some response types from BadRequest to UnprocessableEntity --- basyx/aas/adapter/http.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 93da59ac2..5641720e1 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -18,7 +18,7 @@ import werkzeug.exceptions import werkzeug.routing import werkzeug.urls -from werkzeug.exceptions import BadRequest, Conflict, NotFound +from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response @@ -258,7 +258,7 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError) as e: while e.__cause__ is not None: e = e.__cause__ - raise BadRequest(str(e)) from e + raise UnprocessableEntity(str(e)) from e assert isinstance(rv, expect_type) return rv @@ -488,7 +488,7 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: try: submodel_identifier = sm_ref.get_identifier() except ValueError as e: - raise BadRequest(f"Can't resolve submodel identifier for given reference!") from e + raise UnprocessableEntity(f"Can't resolve submodel identifier for given reference!") from e if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") aas.submodel.add(sm_ref) @@ -626,10 +626,10 @@ def put_submodel_submodel_elements_specific_nested(self, request: Request, url_a mismatch_error_message = f" of new submodel element {new_submodel_element} doesn't not match " \ f"the current submodel element {submodel_element}" if type(submodel_element) is not type(new_submodel_element): - raise BadRequest("Type" + mismatch_error_message) + raise UnprocessableEntity("Type" + mismatch_error_message) # compare id_shorts case-insensitively if submodel_element.id_short.lower() != new_submodel_element.id_short.lower(): - raise BadRequest("id_short" + mismatch_error_message) + raise UnprocessableEntity("id_short" + mismatch_error_message) submodel_element.update_from(new_submodel_element) submodel_element.commit() return response_t(Result(submodel_element)) @@ -658,7 +658,7 @@ def route(request: Request, url_args: Dict, **_kwargs) -> Response: submodel.update() submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) if not isinstance(submodel_element, type_): - raise BadRequest(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") + raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") return response_t(Result(tuple(getattr(submodel_element, attr)))) return route @@ -674,7 +674,7 @@ def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response id_shorts = url_args["id_shorts"] submodel_element = self._get_nested_submodel_element(submodel, id_shorts) if not isinstance(submodel_element, type_): - raise BadRequest(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") + raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") new_submodel_element = parse_request_body(request, request_body_type) if new_submodel_element.id_short in getattr(submodel_element, attr): raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") From e471bd779c6da5403c47916140e9262776c41786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:18:36 +0100 Subject: [PATCH 032/157] adapter.http: refactor submodel element route map --- basyx/aas/adapter/http.py | 54 +++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 5641720e1..efa15a80b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -349,31 +349,35 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/submodelElements", methods=["POST"], endpoint=self.post_submodel_submodel_elements), - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_specific_nested), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_specific_nested), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_specific_nested), - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - Rule("//values", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("//values", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("//annotations", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation")), - Rule("//annotations", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation", - request_body_type=model.DataElement)), # type: ignore - Rule("//statements", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, "statement")), - Rule("//statements", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, "statement")) + Submount("/", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_specific_nested), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_specific_nested), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_specific_nested), + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + Rule("/values", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("/values", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("/annotations", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation")), + Rule("/annotations", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation", + request_body_type=model.DataElement)), # type: ignore + Rule("/statements", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, + "statement")), + Rule("/statements", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, + "statement")), + ]) ]) ]) ], converters={ From 6ce1aaf657ab234d599a44be9ba5daf8974aef4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:52:47 +0100 Subject: [PATCH 033/157] adapter.http: drop support for formulas --- basyx/aas/adapter/http.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index efa15a80b..c1d718b0f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -181,8 +181,6 @@ def aas_object_to_xml(obj: object) -> etree.Element: # TODO: xml serialization needs a constraint_to_xml() function if isinstance(obj, model.Qualifier): return xml_serialization.qualifier_to_xml(obj) - if isinstance(obj, model.Formula): - return xml_serialization.formula_to_xml(obj) if isinstance(obj, model.SubmodelElement): return xml_serialization.submodel_element_to_xml(obj) raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") @@ -230,7 +228,7 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: type_constructables_map = { model.AASReference: XMLConstructables.AAS_REFERENCE, model.View: XMLConstructables.VIEW, - model.Constraint: XMLConstructables.CONSTRAINT, + model.Qualifier: XMLConstructables.QUALIFIER, model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT } From 00b65989bef804a106bb7316e6b86e89858eb6f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:54:46 +0100 Subject: [PATCH 034/157] adapter.http: allow changing id_short in PUT routes --- basyx/aas/adapter/http.py | 46 ++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index c1d718b0f..adcb2c755 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -553,21 +553,26 @@ def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> raise NotFound(f"No view with id_short {view_idshort} found!") return response_t(Result(view)) - def put_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_aas_views_specific(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas_identifier = url_args["aas_id"] + aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] view = aas.view.get("id_short", view_idshort) if view is None: raise NotFound(f"No view with id_short {view_idshort} found!") new_view = parse_request_body(request, model.View) - # compare id_shorts case-insensitively - if new_view.id_short.lower() != view.id_short.lower(): - raise BadRequest(f"id_short of new {new_view} doesn't match the old {view}") - aas.view.remove(view) - aas.view.add(new_view) - return response_t(Result(new_view)) + # TODO: raise conflict if the following fails + view.update_from(new_view) + view.commit() + if view_idshort.upper() != view.id_short.upper(): + created_resource_url = map_adapter.build(self.put_aas_views_specific, { + "aas_id": aas_identifier, + "view_idshort": view.id_short + }, force_external=True) + return response_t(Result(view), status=201, headers={"Location": created_resource_url}) + return response_t(Result(view)) def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) @@ -617,23 +622,30 @@ def get_submodel_submodel_elements_specific_nested(self, request: Request, url_a submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(Result(submodel_element)) - def put_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, + map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + id_short_path = url_args["id_shorts"] + submodel_element = self._get_nested_submodel_element(submodel, id_short_path) + current_id_short = submodel_element.id_short # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 new_submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - mismatch_error_message = f" of new submodel element {new_submodel_element} doesn't not match " \ - f"the current submodel element {submodel_element}" if type(submodel_element) is not type(new_submodel_element): - raise UnprocessableEntity("Type" + mismatch_error_message) - # compare id_shorts case-insensitively - if submodel_element.id_short.lower() != new_submodel_element.id_short.lower(): - raise UnprocessableEntity("id_short" + mismatch_error_message) + raise UnprocessableEntity(f"Type of new submodel element {new_submodel_element} doesn't not match " + f"the current submodel element {submodel_element}") + # TODO: raise conflict if the following fails submodel_element.update_from(new_submodel_element) submodel_element.commit() + if new_submodel_element.id_short.upper() != current_id_short.upper(): + created_resource_url = map_adapter.build(self.put_submodel_submodel_elements_specific_nested, { + "submodel_id": submodel_identifier, + "id_shorts": id_short_path[:-1] + [submodel_element.id_short] + }, force_external=True) + return response_t(Result(submodel_element), status=201, headers={"Location": created_resource_url}) return response_t(Result(submodel_element)) def delete_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) \ From 921c774a698977e37453f45206b39abbc04cd4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 1 Mar 2021 06:55:52 +0100 Subject: [PATCH 035/157] adapter.http: add constraint routes --- basyx/aas/adapter/http.py | 112 +++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index adcb2c755..97432daf5 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -375,7 +375,24 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/statements", methods=["POST"], endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, "statement")), - ]) + Rule("/constraints", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/constraints", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]), + Rule("/constraints", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/constraints", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("/constraints/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints) ]) ]) ], converters={ @@ -420,9 +437,17 @@ def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, i f"is not a submodel element!") current_namespace = next_obj if not isinstance(current_namespace, model.SubmodelElement): - raise TypeError("No id_shorts specified!") + raise ValueError("No id_shorts specified!") return current_namespace + @classmethod + def _get_submodel_or_nested_submodel_element(cls, submodel: model.Submodel, id_shorts: List[str]) \ + -> Union[model.Submodel, model.SubmodelElement]: + try: + return cls._get_nested_submodel_element(submodel, id_shorts) + except ValueError: + return submodel + @classmethod def _expect_namespace(cls, obj: object, needle: str) -> model.UniqueIdShortNamespace: if not isinstance(obj, model.UniqueIdShortNamespace): @@ -663,6 +688,89 @@ def delete_submodel_submodel_elements_specific_nested(self, request: Request, ur self._namespace_submodel_element_op(parent, parent.remove_referable, id_shorts[-1]) return response_t(Result(None)) + def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) + qualifier_type = url_args.get("qualifier_type") + if qualifier_type is None: + return response_t(Result(tuple(sm_or_se.qualifier))) + try: + return response_t(Result(sm_or_se.get_qualifier_by_type(qualifier_type))) + except KeyError: + raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + + def post_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ + -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + id_shorts: List[str] = url_args.get("id_shorts", []) + sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + qualifier = parse_request_body(request, model.Qualifier) + if ("type", qualifier.type) in sm_or_se.qualifier: + raise Conflict(f"Qualifier with type {qualifier.type} already exists!") + sm_or_se.qualifier.add(qualifier) + sm_or_se.commit() + created_resource_url = map_adapter.build(self.get_submodel_submodel_element_constraints, { + "submodel_id": submodel_identifier, + "id_shorts": id_shorts if len(id_shorts) != 0 else None, + "qualifier_type": qualifier.type + }, force_external=True) + return response_t(Result(qualifier), status=201, headers={"Location": created_resource_url}) + + def put_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ + -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + id_shorts: List[str] = url_args.get("id_shorts", []) + sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + new_qualifier = parse_request_body(request, model.Qualifier) + qualifier_type = url_args["qualifier_type"] + try: + qualifier = sm_or_se.get_qualifier_by_type(qualifier_type) + except KeyError: + raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + if type(qualifier) is not type(new_qualifier): + raise UnprocessableEntity(f"Type of new qualifier {new_qualifier} doesn't not match " + f"the current submodel element {qualifier}") + qualifier_type_changed = qualifier_type != new_qualifier.type + # TODO: have to pass a tuple to __contains__ here. can't this be simplified? + if qualifier_type_changed and ("type", new_qualifier.type) in sm_or_se.qualifier: + raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") + sm_or_se.remove_qualifier_by_type(qualifier.type) + sm_or_se.qualifier.add(new_qualifier) + sm_or_se.commit() + if qualifier_type_changed: + created_resource_url = map_adapter.build(self.get_submodel_submodel_element_constraints, { + "submodel_id": submodel_identifier, + "id_shorts": id_shorts if len(id_shorts) != 0 else None, + "qualifier_type": new_qualifier.type + }, force_external=True) + return response_t(Result(new_qualifier), status=201, headers={"Location": created_resource_url}) + return response_t(Result(new_qualifier)) + + def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + id_shorts: List[str] = url_args.get("id_shorts", []) + sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + qualifier_type = url_args["qualifier_type"] + try: + sm_or_se.remove_qualifier_by_type(qualifier_type) + except KeyError: + raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + sm_or_se.commit() + return response_t(Result(None)) + # --------- SUBMODEL ROUTE FACTORIES --------- def factory_get_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str) \ -> Callable[[Request, Dict], Response]: From 677b883486a4c25bc20f2e51a19d02ee278c6219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 6 Sep 2020 15:01:15 +0200 Subject: [PATCH 036/157] http-api-oas: add first working draft add initial specification discussed in the meeting (03.09.2020) --- spec.yml | 1736 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1736 insertions(+) create mode 100644 spec.yml diff --git a/spec.yml b/spec.yml new file mode 100644 index 000000000..1071c1e75 --- /dev/null +++ b/spec.yml @@ -0,0 +1,1736 @@ +openapi: 3.0.0 +info: + version: '1' + title: pyI40AAS REST API + description: REST API Specification for the pyI40AAS framework. Any identifier variables are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. + termsOfService: None + contact: + name: 'Michael Hoffmeister, Manuel Sauer, Constantin Ziesche' + license: + name: Use under Eclipse Public License 2.0 + url: 'https://www.eclipse.org/legal/epl-2.0/' +servers: +- url: http://{authority}/{basePath}/{api-version} + description: "This is the Server to access the Asset Administration Shell" + variables: + authority: + default: localhost:8080 + description: The authority is the server url (made of IP-Address or DNS-Name, user information, and/or port information) of the hosting environment for the Asset Administration Shell + basePath: + default: api + description: The basePath variable is additional path information for the hosting environment. It may contain the name of an aggregation point like 'shells' and/or API version information and/or tenant-id information, etc. + api-version: + default: v1 + description: The Version of the API-Specification +paths: + '/aas/{identifier}': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + get: + summary: Retrieves the Asset Administration Shell + operationId: RetrieveAssetAdministrationShell + responses: + '200': + description: Success + content: + 'application/json': + schema: + $ref: '#/components/schemas/AssetAdministrationShell' + '404': + description: No Concept Dictionary found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + '/aas/{identifier}/asset': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + get: + summary: Returns a reference to the asset of the Asset Administration Shell + operationId: RetrieveAssetReference + responses: + '200': + description: Success + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + '/aas/{identifier}/submodels': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + get: + summary: Returns a list of references to the submodels of the asset administration shell + operationId: RetrieveAllSubmodelsReferences + responses: + '200': + description: Returns a list of requested Submodels or its links + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + post: + summary: Adds a new Submodel reference to the Asset Administration Shell + operationId: CreateSubmodelReference + requestBody: + description: The serialized Submodel reference + required: true + content: + 'application/json': + schema: + $ref: '#/components/schemas/Reference' + responses: + '201': + description: Submodel reference created successfully + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '409': + description: Submodel reference already exists + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + '/aas/{identifier}/views': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + get: + summary: Retrieves all Views from the Asset Administration Shell + operationId: RetrieveAllViews + responses: + '200': + description: Returns a list of requested Views or its links + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + post: + summary: Adds a new View + operationId: CreateOrUpdateView + requestBody: + description: The serialized View + required: true + content: + 'application/json': + schema: + $ref: '#/components/schemas/View' + responses: + '201': + description: View created successfully + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '409': + description: View with given idShort already exists + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + '/aas/{identifier}/views/{view-idShort}': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + - name: view-idShort + in: path + description: 'idShort of the view' + required: true + schema: + type: string + get: + summary: Retrieves a specific View from the Asset Administration Shell + operationId: RetrieveViewByIdShort + responses: + '200': + description: View retrieved successfully + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '404': + description: No View found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + put: + summary: Updates a specific View from the Asset Administration Shell + operationId: UpdateViewByIdShort + responses: + '200': + description: View updated successfully + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '404': + description: No View found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + delete: + summary: Deletes a specific View from the Asset Administration Shell + operationId: DeleteViewByIdShort + responses: + '204': + description: View deleted successfully + '404': + description: No View found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + '/aas/{identifier}/conceptDictionaries': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + get: + summary: Retrieves all Concept Dictionaries of the Asset Administration Shell + operationId: RetrieveAllConceptDictionaries + responses: + '200': + description: Returns a list of all Concept Dictionary + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + post: + summary: Adds a new Concept Dictionary + operationId: CreateConceptDictionary + requestBody: + description: The serialized Concept Dictionary + required: true + content: + 'application/json': + schema: + $ref: '#/components/schemas/ConceptDictionary' + responses: + '201': + description: Concept Dictionary created successfully + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '409': + description: Concept Dictionary with given idShort already exists + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + '/aas/{identifier}/conceptDictionaries/{cd-idShort}': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + - name: cd-idShort + in: path + description: The Concept Dictionary's short id + required: true + schema: + type: string + get: + summary: Retrieves a specific Concept Dictionary + operationId: RetrieveConceptDictionaryByIdShort + responses: + '200': + description: Returns the requested Concept Dictionary + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Result' + '404': + description: No Concept Dictionary found + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + + tags: + - Asset Administration Shell Interface + put: + summary: Update a specific Concept Dictionary + operationId: UpdateConceptDictionaryByIdShort + responses: + '200': + description: Returns the updated Concept Dictionaries + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Result' + '404': + description: No Concept Dictionary found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + delete: + summary: Deletes a specific Concept Dictionary + operationId: DeleteConceptDictionaryByIdShort + responses: + '204': + description: Concept Dictionary deleted successfully + '404': + description: No Concept Dictionary found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Asset Administration Shell Interface + '/submodel/{identifier}': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + get: + summary: Retrieves the entire Submodel + operationId: RetrieveSubmodel + responses: + '200': + description: Success + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '404': + description: No Submodel found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Submodel Interface + '/submodel/{identifier}/submodelElements': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + get: + summary: Retrieves all Submodel-Elements from the current Submodel + operationId: RetrieveAllSubmodelElements + responses: + '200': + description: Returns a list of found Submodel-Elements + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Submodel Interface + post: + summary: Adds a new Submodel-Element to the Submodel + operationId: CreateSubmodelElement + requestBody: + description: The Submodel-Element + required: true + content: + 'application/json': + schema: + $ref: '#/components/schemas/SubmodelElement' + responses: + '201': + description: Submodel-Element created successfully + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '409': + description: Submodel-Element with given idShort already exists + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Submodel Interface + '/submodel/{identifier}/submodelElements/{se-idShort}': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + - name: se-idShort + in: path + description: The Submodel-Element's short id + required: true + schema: + type: string + get: + summary: Retrieves a specific Submodel-Element from the Submodel + operationId: GetSubmodelElementByIdShort + responses: + '200': + description: Returns the requested Submodel-Element + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '404': + description: Submodel-Element not found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Submodel Interface + put: + summary: Update a specific Submodel Element + operationId: UpdateSubmodelElementByIdShort + requestBody: + description: The Submodel-Element + required: true + content: + 'application/json': + schema: + $ref: '#/components/schemas/SubmodelElement' + responses: + '200': + description: Returns the updated Submodel-Element + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Result' + '404': + description: No Submodel-Element found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Submodel Interface + delete: + summary: Deletes a specific Submodel-Element from the Submodel + operationId: DeleteSubmodelElementByIdShort + responses: + '204': + description: Submodel-Element deleted successfully + '404': + description: Submodel-Element not found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Submodel Interface + '/conceptDictionary/{identifier}': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + get: + summary: Retrieve Concept Dictionary by Identifier + operationId: RetrieveConceptDictionary + responses: + '200': + description: Return the requested Concept Dictionary + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '404': + description: No Concept Dictionary found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Concept Dictionary Interface + '/conceptDictionary/{identifier}/conceptDescriptions': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + get: + summary: Retrieve all Concept Descriptions of the Concept Dictionary + operationId: RetrieveConceptDescriptions + responses: + '200': + description: Return Concept Descriptions + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Concept Dictionary Interface + post: + summary: Creates a new Concept Description + operationId: CreateOrUpdateConceptDescription + requestBody: + description: The serialized Concept Description + required: true + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + responses: + '201': + description: Concept Description created successfully + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '409': + description: Concept Description already exists + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Concept Dictionary Interface + '/conceptDictionary/{identifier}/conceptDescriptions/{cd-idShort}': + parameters: + - name: identifier + in: path + description: 'Serialized identifier' + required: true + schema: + type: string + - name: cd-idShort + in: path + description: The Concept Description's short id + required: true + schema: + type: string + get: + summary: Retrieves a specific Concept Description + operationId: RetrieveConceptDescriptionByIdShort + responses: + '200': + description: Return the requested Concept Description + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '404': + description: No Concept Description found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Concept Dictionary Interface + put: + summary: Updates a specific Concept Description + operationId: UpdateConceptDescriptionByIdShort + requestBody: + description: The Concept Description + required: true + content: + 'application/json': + schema: + $ref: '#/components/schemas/ConceptDescription' + responses: + '200': + description: Concept Description updated successfully + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + '404': + description: No Concept Description found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Concept Dictionary Interface + delete: + summary: Deletes a specific Concept Description + operationId: DeleteConceptDescriptionByIdShort + responses: + '204': + description: Concept Description deleted successfully + '404': + description: No Concept Description found + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + default: + description: Unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Result' + tags: + - Concept Dictionary Interface +components: + schemas: + Result: + type: object + properties: + success: + type: boolean + readOnly: true + error: + type: object + nullable: true + readOnly: true + properties: + errorType: + enum: + - Unspecified + - Debug + - Information + - Warning + - Error + - Fatal + - Exception + type: string + code: + type: string + text: + type: string + data: + nullable: true + readOnly: true + oneOf: + - $ref: '#/components/schemas/Referable' + - type: array + items: + $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/Reference' + - type: array + items: + $ref: '#/components/schemas/Reference' + OperationRequest: + type: object + properties: + requestId: + type: string + callbackUrls: + type: object + properties: + failedUrl: + type: string + successUrl: + type: string + params: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + EventSubscription: + type: object + properties: + clientId: + type: string + endpoint: + $ref: '#/components/schemas/Endpoint' + AssetAdministrationShellDescriptor: + type: object + properties: + idShort: + type: string + identification: + $ref: '#/components/schemas/Identifier' + administration: + $ref: '#/components/schemas/AdministrativeInformation' + description: + type: array + items: + $ref: '#/components/schemas/LangString' + asset: + $ref: '#/components/schemas/Asset' + endpoints: + type: array + items: + $ref: '#/components/schemas/Endpoint' + submodelDescriptors: + type: array + items: + $ref: '#/components/schemas/SubmodelDescriptor' + SubmodelDescriptor: + type: object + properties: + idShort: + type: string + identification: + $ref: '#/components/schemas/Identifier' + administration: + $ref: '#/components/schemas/AdministrativeInformation' + description: + type: array + items: + $ref: '#/components/schemas/LangString' + semanticId: + $ref: '#/components/schemas/Reference' + endpoints: + type: array + items: + $ref: '#/components/schemas/Endpoint' + Endpoint: + type: object + properties: + address: + type: string + type: + type: string + parameters: + type: object + Referable: + type: object + properties: + idShort: + type: string + category: + type: string + description: + type: array + items: + "$ref": "#/components/schemas/LangString" + parent: + "$ref": "#/components/schemas/Reference" + modelType: + "$ref": "#/components/schemas/ModelType" + required: + - idShort + - modelType + Identifiable: + allOf: + - "$ref": "#/components/schemas/Referable" + - properties: + identification: + "$ref": "#/components/schemas/Identifier" + administration: + "$ref": "#/components/schemas/AdministrativeInformation" + required: + - identification + Qualifiable: + type: object + properties: + qualifiers: + type: array + items: + "$ref": "#/components/schemas/Constraint" + HasSemantics: + type: object + properties: + semanticId: + "$ref": "#/components/schemas/Reference" + required: + - semanticId + HasDataSpecification: + type: object + properties: + embeddedDataSpecifications: + type: array + items: + "$ref": "#/components/schemas/EmbeddedDataSpecification" + AssetAdministrationShell: + allOf: + - "$ref": "#/components/schemas/Identifiable" + - "$ref": "#/components/schemas/HasDataSpecification" + - properties: + derivedFrom: + "$ref": "#/components/schemas/Reference" + asset: + "$ref": "#/components/schemas/Reference" + submodels: + type: array + items: + "$ref": "#/components/schemas/Reference" + views: + type: array + items: + "$ref": "#/components/schemas/View" + conceptDictionaries: + type: array + items: + "$ref": "#/components/schemas/ConceptDictionary" + security: + "$ref": "#/components/schemas/Security" + required: + - asset + Identifier: + type: object + properties: + id: + type: string + idType: + "$ref": "#/components/schemas/KeyType" + required: + - id + - idType + KeyType: + type: string + enum: + - Custom + - IRDI + - IRI + - IdShort + - FragmentId + AdministrativeInformation: + type: object + properties: + version: + type: string + revision: + type: string + LangString: + type: object + properties: + language: + type: string + text: + type: string + required: + - language + - text + Reference: + type: object + properties: + keys: + type: array + items: + "$ref": "#/components/schemas/Key" + required: + - keys + Key: + type: object + properties: + type: + "$ref": "#/components/schemas/KeyElements" + idType: + "$ref": "#/components/schemas/KeyType" + value: + type: string + local: + type: boolean + required: + - type + - idType + - value + - local + KeyElements: + type: string + enum: + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - ConceptDictionary + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference + ModelTypes: + type: string + enum: + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - ConceptDictionary + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference + - Constraint + - Formula + - Qualifier + ModelType: + type: object + properties: + name: + "$ref": "#/components/schemas/ModelTypes" + required: + - name + EmbeddedDataSpecification: + type: object + properties: + dataSpecification: + "$ref": "#/components/schemas/Reference" + dataSpecificationContent: + "$ref": "#/components/schemas/DataSpecificationContent" + required: + - dataSpecification + - dataSpecificationContent + DataSpecificationContent: + oneOf: + - "$ref": "#/components/schemas/DataSpecificationIEC61360Content" + - "$ref": "#/components/schemas/DataSpecificationPhysicalUnitContent" + DataSpecificationPhysicalUnitContent: + type: object + properties: + unitName: + type: string + unitSymbol: + type: string + definition: + type: array + items: + "$ref": "#/components/schemas/LangString" + siNotation: + type: string + siName: + type: string + dinNotation: + type: string + eceName: + type: string + eceCode: + type: string + nistName: + type: string + sourceOfDefinition: + type: string + conversionFactor: + type: string + registrationAuthorityId: + type: string + supplier: + type: string + required: + - unitName + - unitSymbol + - definition + DataSpecificationIEC61360Content: + allOf: + - "$ref": "#/components/schemas/ValueObject" + - type: object + properties: + dataType: + enum: + - DATE + - STRING + - STRING_TRANSLATABLE + - REAL_MEASURE + - REAL_COUNT + - REAL_CURRENCY + - BOOLEAN + - URL + - RATIONAL + - RATIONAL_MEASURE + - TIME + - TIMESTAMP + definition: + type: array + items: + "$ref": "#/components/schemas/LangString" + preferredName: + type: array + items: + "$ref": "#/components/schemas/LangString" + shortName: + type: array + items: + "$ref": "#/components/schemas/LangString" + sourceOfDefinition: + type: string + symbol: + type: string + unit: + type: string + unitId: + "$ref": "#/components/schemas/Reference" + valueFormat: + type: string + valueList: + "$ref": "#/components/schemas/ValueList" + levelType: + type: array + items: + "$ref": "#/components/schemas/LevelType" + required: + - preferredName + LevelType: + type: string + enum: + - Min + - Max + - Nom + - Typ + ValueList: + type: object + properties: + valueReferencePairTypes: + type: array + minItems: 1 + items: + "$ref": "#/components/schemas/ValueReferencePairType" + required: + - valueReferencePairTypes + ValueReferencePairType: + allOf: + - "$ref": "#/components/schemas/ValueObject" + ValueObject: + type: object + properties: + value: + type: string + valueId: + "$ref": "#/components/schemas/Reference" + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + Asset: + allOf: + - "$ref": "#/components/schemas/Identifiable" + - "$ref": "#/components/schemas/HasDataSpecification" + - properties: + kind: + "$ref": "#/components/schemas/AssetKind" + assetIdentificationModel: + "$ref": "#/components/schemas/Reference" + billOfMaterial: + "$ref": "#/components/schemas/Reference" + required: + - kind + AssetKind: + type: string + enum: + - Type + - Instance + ModelingKind: + type: string + enum: + - Template + - Instance + Submodel: + allOf: + - "$ref": "#/components/schemas/Identifiable" + - "$ref": "#/components/schemas/HasDataSpecification" + - "$ref": "#/components/schemas/Qualifiable" + - "$ref": "#/components/schemas/HasSemantics" + - properties: + kind: + "$ref": "#/components/schemas/ModelingKind" + submodelElements: + type: array + items: + "$ref": "#/components/schemas/SubmodelElement" + Constraint: + type: object + properties: + modelType: + "$ref": "#/components/schemas/ModelType" + required: + - modelType + Operation: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + inputVariable: + type: array + items: + "$ref": "#/components/schemas/OperationVariable" + outputVariable: + type: array + items: + "$ref": "#/components/schemas/OperationVariable" + inoutputVariable: + type: array + items: + "$ref": "#/components/schemas/OperationVariable" + OperationVariable: + type: object + properties: + value: + oneOf: + - "$ref": "#/components/schemas/Blob" + - "$ref": "#/components/schemas/File" + - "$ref": "#/components/schemas/Capability" + - "$ref": "#/components/schemas/Entity" + - "$ref": "#/components/schemas/Event" + - "$ref": "#/components/schemas/BasicEvent" + - "$ref": "#/components/schemas/MultiLanguageProperty" + - "$ref": "#/components/schemas/Operation" + - "$ref": "#/components/schemas/Property" + - "$ref": "#/components/schemas/Range" + - "$ref": "#/components/schemas/ReferenceElement" + - "$ref": "#/components/schemas/RelationshipElement" + - "$ref": "#/components/schemas/SubmodelElementCollection" + required: + - value + SubmodelElement: + allOf: + - "$ref": "#/components/schemas/Referable" + - "$ref": "#/components/schemas/HasDataSpecification" + - "$ref": "#/components/schemas/HasSemantics" + - "$ref": "#/components/schemas/Qualifiable" + - properties: + kind: + "$ref": "#/components/schemas/ModelingKind" + Event: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + BasicEvent: + allOf: + - "$ref": "#/components/schemas/Event" + - properties: + observed: + "$ref": "#/components/schemas/Reference" + required: + - observed + EntityType: + type: string + enum: + - CoManagedEntity + - SelfManagedEntity + Entity: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + statements: + type: array + items: + "$ref": "#/components/schemas/SubmodelElement" + entityType: + "$ref": "#/components/schemas/EntityType" + asset: + "$ref": "#/components/schemas/Reference" + required: + - entityType + View: + allOf: + - "$ref": "#/components/schemas/Referable" + - "$ref": "#/components/schemas/HasDataSpecification" + - "$ref": "#/components/schemas/HasSemantics" + - properties: + containedElements: + type: array + items: + "$ref": "#/components/schemas/Reference" + ConceptDictionary: + allOf: + - "$ref": "#/components/schemas/Referable" + - "$ref": "#/components/schemas/HasDataSpecification" + - properties: + conceptDescriptions: + type: array + items: + "$ref": "#/components/schemas/Reference" + ConceptDescription: + allOf: + - "$ref": "#/components/schemas/Identifiable" + - "$ref": "#/components/schemas/HasDataSpecification" + - properties: + isCaseOf: + type: array + items: + "$ref": "#/components/schemas/Reference" + Capability: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + Property: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - "$ref": "#/components/schemas/ValueObject" + Range: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + min: + type: string + max: + type: string + required: + - valueType + - min + - max + MultiLanguageProperty: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + type: array + items: + "$ref": "#/components/schemas/LangString" + valueId: + "$ref": "#/components/schemas/Reference" + File: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + - value + Blob: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + - value + ReferenceElement: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + "$ref": "#/components/schemas/Reference" + SubmodelElementCollection: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + type: array + items: + oneOf: + - "$ref": "#/components/schemas/Blob" + - "$ref": "#/components/schemas/File" + - "$ref": "#/components/schemas/Capability" + - "$ref": "#/components/schemas/Entity" + - "$ref": "#/components/schemas/Event" + - "$ref": "#/components/schemas/BasicEvent" + - "$ref": "#/components/schemas/MultiLanguageProperty" + - "$ref": "#/components/schemas/Operation" + - "$ref": "#/components/schemas/Property" + - "$ref": "#/components/schemas/Range" + - "$ref": "#/components/schemas/ReferenceElement" + - "$ref": "#/components/schemas/RelationshipElement" + - "$ref": "#/components/schemas/SubmodelElementCollection" + allowDuplicates: + type: boolean + ordered: + type: boolean + RelationshipElement: + allOf: + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + first: + "$ref": "#/components/schemas/Reference" + second: + "$ref": "#/components/schemas/Reference" + required: + - first + - second + AnnotatedRelationshipElement: + allOf: + - "$ref": "#/components/schemas/RelationshipElement" + - properties: + annotation: + type: array + items: + "$ref": "#/components/schemas/Reference" + Qualifier: + allOf: + - "$ref": "#/components/schemas/Constraint" + - "$ref": "#/components/schemas/HasSemantics" + - "$ref": "#/components/schemas/ValueObject" + - properties: + type: + type: string + required: + - type + Formula: + allOf: + - "$ref": "#/components/schemas/Constraint" + - properties: + dependsOn: + type: array + items: + "$ref": "#/components/schemas/Reference" + Security: + type: object + properties: + accessControlPolicyPoints: + "$ref": "#/components/schemas/AccessControlPolicyPoints" + certificate: + type: array + items: + oneOf: + - "$ref": "#/components/schemas/BlobCertificate" + requiredCertificateExtension: + type: array + items: + "$ref": "#/components/schemas/Reference" + required: + - accessControlPolicyPoints + Certificate: + type: object + BlobCertificate: + allOf: + - "$ref": "#/components/schemas/Certificate" + - properties: + blobCertificate: + "$ref": "#/components/schemas/Blob" + containedExtension: + type: array + items: + "$ref": "#/components/schemas/Reference" + lastCertificate: + type: boolean + AccessControlPolicyPoints: + type: object + properties: + policyAdministrationPoint: + "$ref": "#/components/schemas/PolicyAdministrationPoint" + policyDecisionPoint: + "$ref": "#/components/schemas/PolicyDecisionPoint" + policyEnforcementPoint: + "$ref": "#/components/schemas/PolicyEnforcementPoint" + policyInformationPoints: + "$ref": "#/components/schemas/PolicyInformationPoints" + required: + - policyAdministrationPoint + - policyDecisionPoint + - policyEnforcementPoint + PolicyAdministrationPoint: + type: object + properties: + localAccessControl: + "$ref": "#/components/schemas/AccessControl" + externalAccessControl: + type: boolean + required: + - externalAccessControl + PolicyInformationPoints: + type: object + properties: + internalInformationPoint: + type: array + items: + "$ref": "#/components/schemas/Reference" + externalInformationPoint: + type: boolean + required: + - externalInformationPoint + PolicyEnforcementPoint: + type: object + properties: + externalPolicyEnforcementPoint: + type: boolean + required: + - externalPolicyEnforcementPoint + PolicyDecisionPoint: + type: object + properties: + externalPolicyDecisionPoints: + type: boolean + required: + - externalPolicyDecisionPoints + AccessControl: + type: object + properties: + selectableSubjectAttributes: + "$ref": "#/components/schemas/Reference" + defaultSubjectAttributes: + "$ref": "#/components/schemas/Reference" + selectablePermissions: + "$ref": "#/components/schemas/Reference" + defaultPermissions: + "$ref": "#/components/schemas/Reference" + selectableEnvironmentAttributes: + "$ref": "#/components/schemas/Reference" + defaultEnvironmentAttributes: + "$ref": "#/components/schemas/Reference" + accessPermissionRule: + type: array + items: + "$ref": "#/components/schemas/AccessPermissionRule" + AccessPermissionRule: + allOf: + - "$ref": "#/components/schemas/Referable" + - "$ref": "#/components/schemas/Qualifiable" + - properties: + targetSubjectAttributes: + type: array + items: + "$ref": "#/components/schemas/SubjectAttributes" + minItems: 1 + permissionsPerObject: + type: array + items: + "$ref": "#/components/schemas/PermissionsPerObject" + required: + - targetSubjectAttributes + SubjectAttributes: + type: object + properties: + subjectAttributes: + type: array + items: + "$ref": "#/components/schemas/Reference" + minItems: 1 + PermissionsPerObject: + type: object + properties: + object: + "$ref": "#/components/schemas/Reference" + targetObjectAttributes: + "$ref": "#/components/schemas/ObjectAttributes" + permission: + type: array + items: + "$ref": "#/components/schemas/Permission" + ObjectAttributes: + type: object + properties: + objectAttribute: + type: array + items: + "$ref": "#/components/schemas/Property" + minItems: 1 + Permission: + type: object + properties: + permission: + "$ref": "#/components/schemas/Reference" + kindOfPermission: + type: string + enum: + - Allow + - Deny + - NotApplicable + - Undefined + required: + - permission + - kindOfPermission \ No newline at end of file From e8ec2a7ba2874ea16027f0467b16b94f47915241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 6 Sep 2020 16:39:56 +0200 Subject: [PATCH 037/157] http-api-oas: make /aas /submodel /conceptDictionary return a stripped object --- spec.yml | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/spec.yml b/spec.yml index 1071c1e75..04f2b4a57 100644 --- a/spec.yml +++ b/spec.yml @@ -5,7 +5,7 @@ info: description: REST API Specification for the pyI40AAS framework. Any identifier variables are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. termsOfService: None contact: - name: 'Michael Hoffmeister, Manuel Sauer, Constantin Ziesche' + name: 'Michael Hoffmeister, Manuel Sauer, Constantin Ziesche, Leon Möller' license: name: Use under Eclipse Public License 2.0 url: 'https://www.eclipse.org/legal/epl-2.0/' @@ -32,7 +32,7 @@ paths: schema: type: string get: - summary: Retrieves the Asset Administration Shell + summary: Retrieves the stripped Asset Administration Shell, without Submodel References, Views and Concept Dictionaries. operationId: RetrieveAssetAdministrationShell responses: '200': @@ -40,7 +40,7 @@ paths: content: 'application/json': schema: - $ref: '#/components/schemas/AssetAdministrationShell' + $ref: '#/components/schemas/Result' '404': description: No Concept Dictionary found content: @@ -55,32 +55,6 @@ paths: $ref: '#/components/schemas/Result' tags: - Asset Administration Shell Interface - '/aas/{identifier}/asset': - parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string - get: - summary: Returns a reference to the asset of the Asset Administration Shell - operationId: RetrieveAssetReference - responses: - '200': - description: Success - content: - 'application/json': - schema: - $ref: '#/components/schemas/Result' - default: - description: Unexpected error - content: - 'application/json': - schema: - $ref: '#/components/schemas/Result' - tags: - - Asset Administration Shell Interface '/aas/{identifier}/submodels': parameters: - name: identifier @@ -436,7 +410,7 @@ paths: schema: type: string get: - summary: Retrieves the entire Submodel + summary: Retrieves the stripped Submodel (without submodel elements) operationId: RetrieveSubmodel responses: '200': @@ -612,7 +586,7 @@ paths: schema: type: string get: - summary: Retrieve Concept Dictionary by Identifier + summary: Returns the stripped Concept Dictionary (without Concept Descriptions) operationId: RetrieveConceptDictionary responses: '200': From 59a689f234ea6528309d68212b519830f65af94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 8 Sep 2020 19:30:05 +0200 Subject: [PATCH 038/157] http-api-oas: formatting --- spec.yml | 1674 +++++++++++++++++++++++++++--------------------------- 1 file changed, 837 insertions(+), 837 deletions(-) diff --git a/spec.yml b/spec.yml index 04f2b4a57..0e9b9f0f3 100644 --- a/spec.yml +++ b/spec.yml @@ -1,86 +1,86 @@ openapi: 3.0.0 info: - version: '1' + version: "1" title: pyI40AAS REST API description: REST API Specification for the pyI40AAS framework. Any identifier variables are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. termsOfService: None contact: - name: 'Michael Hoffmeister, Manuel Sauer, Constantin Ziesche, Leon Möller' + name: "Michael Hoffmeister, Manuel Sauer, Constantin Ziesche, Leon Möller" license: name: Use under Eclipse Public License 2.0 - url: 'https://www.eclipse.org/legal/epl-2.0/' -servers: -- url: http://{authority}/{basePath}/{api-version} - description: "This is the Server to access the Asset Administration Shell" - variables: - authority: - default: localhost:8080 - description: The authority is the server url (made of IP-Address or DNS-Name, user information, and/or port information) of the hosting environment for the Asset Administration Shell - basePath: - default: api - description: The basePath variable is additional path information for the hosting environment. It may contain the name of an aggregation point like 'shells' and/or API version information and/or tenant-id information, etc. - api-version: - default: v1 - description: The Version of the API-Specification + url: "https://www.eclipse.org/legal/epl-2.0/" +servers: + - url: http://{authority}/{basePath}/{api-version} + description: "This is the Server to access the Asset Administration Shell" + variables: + authority: + default: localhost:8080 + description: The authority is the server url (made of IP-Address or DNS-Name, user information, and/or port information) of the hosting environment for the Asset Administration Shell + basePath: + default: api + description: The basePath variable is additional path information for the hosting environment. It may contain the name of an aggregation point like 'shells' and/or API version information and/or tenant-id information, etc. + api-version: + default: v1 + description: The Version of the API-Specification paths: - '/aas/{identifier}': + "/aas/{identifier}": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string get: summary: Retrieves the stripped Asset Administration Shell, without Submodel References, Views and Concept Dictionaries. operationId: RetrieveAssetAdministrationShell responses: - '200': + "200": description: Success content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No Concept Dictionary found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface - '/aas/{identifier}/submodels': + "/aas/{identifier}/submodels": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string get: summary: Returns a list of references to the submodels of the asset administration shell operationId: RetrieveAllSubmodelsReferences responses: - '200': + "200": description: Returns a list of requested Submodels or its links content: - 'application/json': + "application/json": schema: type: array items: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface post: @@ -90,56 +90,56 @@ paths: description: The serialized Submodel reference required: true content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" responses: - '201': + "201": description: Submodel reference created successfully content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '409': + $ref: "#/components/schemas/Result" + "409": description: Submodel reference already exists content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface - '/aas/{identifier}/views': + "/aas/{identifier}/views": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string get: summary: Retrieves all Views from the Asset Administration Shell operationId: RetrieveAllViews responses: - '200': + "200": description: Returns a list of requested Views or its links content: - 'application/json': + "application/json": schema: type: array items: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface post: @@ -149,316 +149,316 @@ paths: description: The serialized View required: true content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/View' + $ref: "#/components/schemas/View" responses: - '201': + "201": description: View created successfully content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '409': + $ref: "#/components/schemas/Result" + "409": description: View with given idShort already exists content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface - '/aas/{identifier}/views/{view-idShort}': + "/aas/{identifier}/views/{view-idShort}": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string - - name: view-idShort - in: path - description: 'idShort of the view' - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string + - name: view-idShort + in: path + description: "idShort of the view" + required: true + schema: + type: string get: summary: Retrieves a specific View from the Asset Administration Shell operationId: RetrieveViewByIdShort responses: - '200': + "200": description: View retrieved successfully content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No View found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface put: summary: Updates a specific View from the Asset Administration Shell operationId: UpdateViewByIdShort responses: - '200': + "200": description: View updated successfully content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No View found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface delete: summary: Deletes a specific View from the Asset Administration Shell operationId: DeleteViewByIdShort responses: - '204': + "204": description: View deleted successfully - '404': + "404": description: No View found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface - '/aas/{identifier}/conceptDictionaries': + "/aas/{identifier}/conceptDictionaries": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string get: summary: Retrieves all Concept Dictionaries of the Asset Administration Shell operationId: RetrieveAllConceptDictionaries responses: - '200': + "200": description: Returns a list of all Concept Dictionary content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface post: - summary: Adds a new Concept Dictionary + summary: Adds a new Concept Dictionary operationId: CreateConceptDictionary requestBody: description: The serialized Concept Dictionary required: true content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/ConceptDictionary' + $ref: "#/components/schemas/ConceptDictionary" responses: - '201': + "201": description: Concept Dictionary created successfully content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '409': + $ref: "#/components/schemas/Result" + "409": description: Concept Dictionary with given idShort already exists content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface - '/aas/{identifier}/conceptDictionaries/{cd-idShort}': + "/aas/{identifier}/conceptDictionaries/{cd-idShort}": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string - - name: cd-idShort - in: path - description: The Concept Dictionary's short id - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string + - name: cd-idShort + in: path + description: The Concept Dictionary's short id + required: true + schema: + type: string get: - summary: Retrieves a specific Concept Dictionary + summary: Retrieves a specific Concept Dictionary operationId: RetrieveConceptDictionaryByIdShort responses: - '200': + "200": description: Returns the requested Concept Dictionary content: - 'application/json': + "application/json": schema: type: array items: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No Concept Dictionary found content: - 'application/json': + "application/json": schema: type: array items: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - + $ref: "#/components/schemas/Result" + tags: - Asset Administration Shell Interface put: - summary: Update a specific Concept Dictionary + summary: Update a specific Concept Dictionary operationId: UpdateConceptDictionaryByIdShort responses: - '200': + "200": description: Returns the updated Concept Dictionaries content: - 'application/json': + "application/json": schema: type: array items: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No Concept Dictionary found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface delete: summary: Deletes a specific Concept Dictionary operationId: DeleteConceptDictionaryByIdShort responses: - '204': + "204": description: Concept Dictionary deleted successfully - '404': + "404": description: No Concept Dictionary found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface - '/submodel/{identifier}': + "/submodel/{identifier}": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string get: summary: Retrieves the stripped Submodel (without submodel elements) operationId: RetrieveSubmodel responses: - '200': + "200": description: Success content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No Submodel found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Submodel Interface - '/submodel/{identifier}/submodelElements': + "/submodel/{identifier}/submodelElements": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string get: summary: Retrieves all Submodel-Elements from the current Submodel operationId: RetrieveAllSubmodelElements responses: - '200': + "200": description: Returns a list of found Submodel-Elements content: - 'application/json': + "application/json": schema: type: array items: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Submodel Interface post: @@ -468,240 +468,240 @@ paths: description: The Submodel-Element required: true content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/SubmodelElement' + $ref: "#/components/schemas/SubmodelElement" responses: - '201': + "201": description: Submodel-Element created successfully content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '409': + $ref: "#/components/schemas/Result" + "409": description: Submodel-Element with given idShort already exists content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Submodel Interface - '/submodel/{identifier}/submodelElements/{se-idShort}': + "/submodel/{identifier}/submodelElements/{se-idShort}": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string - - name: se-idShort - in: path - description: The Submodel-Element's short id - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string + - name: se-idShort + in: path + description: The Submodel-Element's short id + required: true + schema: + type: string get: summary: Retrieves a specific Submodel-Element from the Submodel operationId: GetSubmodelElementByIdShort responses: - '200': + "200": description: Returns the requested Submodel-Element content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: Submodel-Element not found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Submodel Interface put: - summary: Update a specific Submodel Element + summary: Update a specific Submodel Element operationId: UpdateSubmodelElementByIdShort requestBody: description: The Submodel-Element required: true content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/SubmodelElement' + $ref: "#/components/schemas/SubmodelElement" responses: - '200': + "200": description: Returns the updated Submodel-Element content: - 'application/json': + "application/json": schema: type: array items: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No Submodel-Element found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Submodel Interface delete: summary: Deletes a specific Submodel-Element from the Submodel operationId: DeleteSubmodelElementByIdShort responses: - '204': + "204": description: Submodel-Element deleted successfully - '404': + "404": description: Submodel-Element not found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Submodel Interface - '/conceptDictionary/{identifier}': + "/conceptDictionary/{identifier}": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string get: summary: Returns the stripped Concept Dictionary (without Concept Descriptions) operationId: RetrieveConceptDictionary responses: - '200': + "200": description: Return the requested Concept Dictionary content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No Concept Dictionary found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Concept Dictionary Interface - '/conceptDictionary/{identifier}/conceptDescriptions': + "/conceptDictionary/{identifier}/conceptDescriptions": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string get: summary: Retrieve all Concept Descriptions of the Concept Dictionary operationId: RetrieveConceptDescriptions responses: - '200': + "200": description: Return Concept Descriptions content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - - Concept Dictionary Interface + - Concept Dictionary Interface post: summary: Creates a new Concept Description - operationId: CreateOrUpdateConceptDescription + operationId: CreateConceptDescription requestBody: description: The serialized Concept Description required: true content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" responses: - '201': + "201": description: Concept Description created successfully content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '409': + $ref: "#/components/schemas/Result" + "409": description: Concept Description already exists content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - - Concept Dictionary Interface - '/conceptDictionary/{identifier}/conceptDescriptions/{cd-idShort}': + - Concept Dictionary Interface + "/conceptDictionary/{identifier}/conceptDescriptions/{cd-idShort}": parameters: - - name: identifier - in: path - description: 'Serialized identifier' - required: true - schema: - type: string - - name: cd-idShort - in: path - description: The Concept Description's short id - required: true - schema: - type: string + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string + - name: cd-idShort + in: path + description: The Concept Description's short id + required: true + schema: + type: string get: summary: Retrieves a specific Concept Description operationId: RetrieveConceptDescriptionByIdShort responses: - '200': + "200": description: Return the requested Concept Description content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No Concept Description found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Concept Dictionary Interface put: @@ -711,48 +711,48 @@ paths: description: The Concept Description required: true content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/ConceptDescription' + $ref: "#/components/schemas/ConceptDescription" responses: - '200': + "200": description: Concept Description updated successfully content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' - '404': + $ref: "#/components/schemas/Result" + "404": description: No Concept Description found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - - Concept Dictionary Interface + - Concept Dictionary Interface delete: summary: Deletes a specific Concept Description operationId: DeleteConceptDescriptionByIdShort responses: - '204': + "204": description: Concept Description deleted successfully - '404': + "404": description: No Concept Description found content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" default: description: Unexpected error content: - 'application/json': + "application/json": schema: - $ref: '#/components/schemas/Result' + $ref: "#/components/schemas/Result" tags: - Concept Dictionary Interface components: @@ -770,13 +770,13 @@ components: properties: errorType: enum: - - Unspecified - - Debug - - Information - - Warning - - Error - - Fatal - - Exception + - Unspecified + - Debug + - Information + - Warning + - Error + - Fatal + - Exception type: string code: type: string @@ -786,14 +786,14 @@ components: nullable: true readOnly: true oneOf: - - $ref: '#/components/schemas/Referable' + - $ref: "#/components/schemas/Referable" - type: array items: - $ref: '#/components/schemas/Referable' - - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Referable" + - $ref: "#/components/schemas/Reference" - type: array items: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" OperationRequest: type: object properties: @@ -809,56 +809,56 @@ components: params: type: array items: - $ref: '#/components/schemas/OperationVariable' + $ref: "#/components/schemas/OperationVariable" EventSubscription: type: object properties: clientId: type: string endpoint: - $ref: '#/components/schemas/Endpoint' + $ref: "#/components/schemas/Endpoint" AssetAdministrationShellDescriptor: type: object properties: idShort: type: string identification: - $ref: '#/components/schemas/Identifier' + $ref: "#/components/schemas/Identifier" administration: - $ref: '#/components/schemas/AdministrativeInformation' + $ref: "#/components/schemas/AdministrativeInformation" description: type: array items: - $ref: '#/components/schemas/LangString' + $ref: "#/components/schemas/LangString" asset: - $ref: '#/components/schemas/Asset' + $ref: "#/components/schemas/Asset" endpoints: type: array items: - $ref: '#/components/schemas/Endpoint' + $ref: "#/components/schemas/Endpoint" submodelDescriptors: type: array items: - $ref: '#/components/schemas/SubmodelDescriptor' + $ref: "#/components/schemas/SubmodelDescriptor" SubmodelDescriptor: type: object properties: idShort: type: string identification: - $ref: '#/components/schemas/Identifier' + $ref: "#/components/schemas/Identifier" administration: - $ref: '#/components/schemas/AdministrativeInformation' + $ref: "#/components/schemas/AdministrativeInformation" description: type: array items: - $ref: '#/components/schemas/LangString' + $ref: "#/components/schemas/LangString" semanticId: - $ref: '#/components/schemas/Reference' + $ref: "#/components/schemas/Reference" endpoints: type: array items: - $ref: '#/components/schemas/Endpoint' + $ref: "#/components/schemas/Endpoint" Endpoint: type: object properties: @@ -884,18 +884,18 @@ components: modelType: "$ref": "#/components/schemas/ModelType" required: - - idShort - - modelType + - idShort + - modelType Identifiable: allOf: - - "$ref": "#/components/schemas/Referable" - - properties: - identification: - "$ref": "#/components/schemas/Identifier" - administration: - "$ref": "#/components/schemas/AdministrativeInformation" - required: - - identification + - "$ref": "#/components/schemas/Referable" + - properties: + identification: + "$ref": "#/components/schemas/Identifier" + administration: + "$ref": "#/components/schemas/AdministrativeInformation" + required: + - identification Qualifiable: type: object properties: @@ -909,7 +909,7 @@ components: semanticId: "$ref": "#/components/schemas/Reference" required: - - semanticId + - semanticId HasDataSpecification: type: object properties: @@ -919,29 +919,29 @@ components: "$ref": "#/components/schemas/EmbeddedDataSpecification" AssetAdministrationShell: allOf: - - "$ref": "#/components/schemas/Identifiable" - - "$ref": "#/components/schemas/HasDataSpecification" - - properties: - derivedFrom: - "$ref": "#/components/schemas/Reference" - asset: - "$ref": "#/components/schemas/Reference" - submodels: - type: array - items: + - "$ref": "#/components/schemas/Identifiable" + - "$ref": "#/components/schemas/HasDataSpecification" + - properties: + derivedFrom: "$ref": "#/components/schemas/Reference" - views: - type: array - items: - "$ref": "#/components/schemas/View" - conceptDictionaries: - type: array - items: - "$ref": "#/components/schemas/ConceptDictionary" - security: - "$ref": "#/components/schemas/Security" - required: - - asset + asset: + "$ref": "#/components/schemas/Reference" + submodels: + type: array + items: + "$ref": "#/components/schemas/Reference" + views: + type: array + items: + "$ref": "#/components/schemas/View" + conceptDictionaries: + type: array + items: + "$ref": "#/components/schemas/ConceptDictionary" + security: + "$ref": "#/components/schemas/Security" + required: + - asset Identifier: type: object properties: @@ -950,16 +950,16 @@ components: idType: "$ref": "#/components/schemas/KeyType" required: - - id - - idType + - id + - idType KeyType: type: string enum: - - Custom - - IRDI - - IRI - - IdShort - - FragmentId + - Custom + - IRDI + - IRI + - IdShort + - FragmentId AdministrativeInformation: type: object properties: @@ -975,8 +975,8 @@ components: text: type: string required: - - language - - text + - language + - text Reference: type: object properties: @@ -985,7 +985,7 @@ components: items: "$ref": "#/components/schemas/Key" required: - - keys + - keys Key: type: object properties: @@ -998,76 +998,76 @@ components: local: type: boolean required: - - type - - idType - - value - - local + - type + - idType + - value + - local KeyElements: type: string enum: - - Asset - - AssetAdministrationShell - - ConceptDescription - - Submodel - - AccessPermissionRule - - AnnotatedRelationshipElement - - BasicEvent - - Blob - - Capability - - ConceptDictionary - - DataElement - - File - - Entity - - Event - - MultiLanguageProperty - - Operation - - Property - - Range - - ReferenceElement - - RelationshipElement - - SubmodelElement - - SubmodelElementCollection - - View - - GlobalReference - - FragmentReference + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - ConceptDictionary + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference ModelTypes: type: string enum: - - Asset - - AssetAdministrationShell - - ConceptDescription - - Submodel - - AccessPermissionRule - - AnnotatedRelationshipElement - - BasicEvent - - Blob - - Capability - - ConceptDictionary - - DataElement - - File - - Entity - - Event - - MultiLanguageProperty - - Operation - - Property - - Range - - ReferenceElement - - RelationshipElement - - SubmodelElement - - SubmodelElementCollection - - View - - GlobalReference - - FragmentReference - - Constraint - - Formula - - Qualifier + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - ConceptDictionary + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference + - Constraint + - Formula + - Qualifier ModelType: type: object properties: name: "$ref": "#/components/schemas/ModelTypes" required: - - name + - name EmbeddedDataSpecification: type: object properties: @@ -1076,12 +1076,12 @@ components: dataSpecificationContent: "$ref": "#/components/schemas/DataSpecificationContent" required: - - dataSpecification - - dataSpecificationContent + - dataSpecification + - dataSpecificationContent DataSpecificationContent: oneOf: - - "$ref": "#/components/schemas/DataSpecificationIEC61360Content" - - "$ref": "#/components/schemas/DataSpecificationPhysicalUnitContent" + - "$ref": "#/components/schemas/DataSpecificationIEC61360Content" + - "$ref": "#/components/schemas/DataSpecificationPhysicalUnitContent" DataSpecificationPhysicalUnitContent: type: object properties: @@ -1114,65 +1114,65 @@ components: supplier: type: string required: - - unitName - - unitSymbol - - definition + - unitName + - unitSymbol + - definition DataSpecificationIEC61360Content: allOf: - - "$ref": "#/components/schemas/ValueObject" - - type: object - properties: - dataType: - enum: - - DATE - - STRING - - STRING_TRANSLATABLE - - REAL_MEASURE - - REAL_COUNT - - REAL_CURRENCY - - BOOLEAN - - URL - - RATIONAL - - RATIONAL_MEASURE - - TIME - - TIMESTAMP - definition: - type: array - items: - "$ref": "#/components/schemas/LangString" - preferredName: - type: array - items: - "$ref": "#/components/schemas/LangString" - shortName: - type: array - items: - "$ref": "#/components/schemas/LangString" - sourceOfDefinition: - type: string - symbol: - type: string - unit: - type: string - unitId: - "$ref": "#/components/schemas/Reference" - valueFormat: - type: string - valueList: - "$ref": "#/components/schemas/ValueList" - levelType: - type: array - items: - "$ref": "#/components/schemas/LevelType" - required: - - preferredName + - "$ref": "#/components/schemas/ValueObject" + - type: object + properties: + dataType: + enum: + - DATE + - STRING + - STRING_TRANSLATABLE + - REAL_MEASURE + - REAL_COUNT + - REAL_CURRENCY + - BOOLEAN + - URL + - RATIONAL + - RATIONAL_MEASURE + - TIME + - TIMESTAMP + definition: + type: array + items: + "$ref": "#/components/schemas/LangString" + preferredName: + type: array + items: + "$ref": "#/components/schemas/LangString" + shortName: + type: array + items: + "$ref": "#/components/schemas/LangString" + sourceOfDefinition: + type: string + symbol: + type: string + unit: + type: string + unitId: + "$ref": "#/components/schemas/Reference" + valueFormat: + type: string + valueList: + "$ref": "#/components/schemas/ValueList" + levelType: + type: array + items: + "$ref": "#/components/schemas/LevelType" + required: + - preferredName LevelType: type: string enum: - - Min - - Max - - Nom - - Typ + - Min + - Max + - Nom + - Typ ValueList: type: object properties: @@ -1182,10 +1182,10 @@ components: items: "$ref": "#/components/schemas/ValueReferencePairType" required: - - valueReferencePairTypes + - valueReferencePairTypes ValueReferencePairType: allOf: - - "$ref": "#/components/schemas/ValueObject" + - "$ref": "#/components/schemas/ValueObject" ValueObject: type: object properties: @@ -1196,362 +1196,362 @@ components: valueType: type: string enum: - - anyUri - - base64Binary - - boolean - - date - - dateTime - - dateTimeStamp - - decimal - - integer - - long - - int - - short - - byte - - nonNegativeInteger - - positiveInteger - - unsignedLong - - unsignedInt - - unsignedShort - - unsignedByte - - nonPositiveInteger - - negativeInteger - - double - - duration - - dayTimeDuration - - yearMonthDuration - - float - - gDay - - gMonth - - gMonthDay - - gYear - - gYearMonth - - hexBinary - - NOTATION - - QName - - string - - normalizedString - - token - - language - - Name - - NCName - - ENTITY - - ID - - IDREF - - NMTOKEN - - time + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time Asset: allOf: - - "$ref": "#/components/schemas/Identifiable" - - "$ref": "#/components/schemas/HasDataSpecification" - - properties: - kind: - "$ref": "#/components/schemas/AssetKind" - assetIdentificationModel: - "$ref": "#/components/schemas/Reference" - billOfMaterial: - "$ref": "#/components/schemas/Reference" - required: - - kind + - "$ref": "#/components/schemas/Identifiable" + - "$ref": "#/components/schemas/HasDataSpecification" + - properties: + kind: + "$ref": "#/components/schemas/AssetKind" + assetIdentificationModel: + "$ref": "#/components/schemas/Reference" + billOfMaterial: + "$ref": "#/components/schemas/Reference" + required: + - kind AssetKind: type: string enum: - - Type - - Instance + - Type + - Instance ModelingKind: type: string enum: - - Template - - Instance + - Template + - Instance Submodel: allOf: - - "$ref": "#/components/schemas/Identifiable" - - "$ref": "#/components/schemas/HasDataSpecification" - - "$ref": "#/components/schemas/Qualifiable" - - "$ref": "#/components/schemas/HasSemantics" - - properties: - kind: - "$ref": "#/components/schemas/ModelingKind" - submodelElements: - type: array - items: - "$ref": "#/components/schemas/SubmodelElement" + - "$ref": "#/components/schemas/Identifiable" + - "$ref": "#/components/schemas/HasDataSpecification" + - "$ref": "#/components/schemas/Qualifiable" + - "$ref": "#/components/schemas/HasSemantics" + - properties: + kind: + "$ref": "#/components/schemas/ModelingKind" + submodelElements: + type: array + items: + "$ref": "#/components/schemas/SubmodelElement" Constraint: type: object properties: modelType: "$ref": "#/components/schemas/ModelType" required: - - modelType + - modelType Operation: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - properties: - inputVariable: - type: array - items: - "$ref": "#/components/schemas/OperationVariable" - outputVariable: - type: array - items: - "$ref": "#/components/schemas/OperationVariable" - inoutputVariable: - type: array - items: - "$ref": "#/components/schemas/OperationVariable" + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + inputVariable: + type: array + items: + "$ref": "#/components/schemas/OperationVariable" + outputVariable: + type: array + items: + "$ref": "#/components/schemas/OperationVariable" + inoutputVariable: + type: array + items: + "$ref": "#/components/schemas/OperationVariable" OperationVariable: type: object properties: value: oneOf: - - "$ref": "#/components/schemas/Blob" - - "$ref": "#/components/schemas/File" - - "$ref": "#/components/schemas/Capability" - - "$ref": "#/components/schemas/Entity" - - "$ref": "#/components/schemas/Event" - - "$ref": "#/components/schemas/BasicEvent" - - "$ref": "#/components/schemas/MultiLanguageProperty" - - "$ref": "#/components/schemas/Operation" - - "$ref": "#/components/schemas/Property" - - "$ref": "#/components/schemas/Range" - - "$ref": "#/components/schemas/ReferenceElement" - - "$ref": "#/components/schemas/RelationshipElement" - - "$ref": "#/components/schemas/SubmodelElementCollection" + - "$ref": "#/components/schemas/Blob" + - "$ref": "#/components/schemas/File" + - "$ref": "#/components/schemas/Capability" + - "$ref": "#/components/schemas/Entity" + - "$ref": "#/components/schemas/Event" + - "$ref": "#/components/schemas/BasicEvent" + - "$ref": "#/components/schemas/MultiLanguageProperty" + - "$ref": "#/components/schemas/Operation" + - "$ref": "#/components/schemas/Property" + - "$ref": "#/components/schemas/Range" + - "$ref": "#/components/schemas/ReferenceElement" + - "$ref": "#/components/schemas/RelationshipElement" + - "$ref": "#/components/schemas/SubmodelElementCollection" required: - - value + - value SubmodelElement: allOf: - - "$ref": "#/components/schemas/Referable" - - "$ref": "#/components/schemas/HasDataSpecification" - - "$ref": "#/components/schemas/HasSemantics" - - "$ref": "#/components/schemas/Qualifiable" - - properties: - kind: - "$ref": "#/components/schemas/ModelingKind" + - "$ref": "#/components/schemas/Referable" + - "$ref": "#/components/schemas/HasDataSpecification" + - "$ref": "#/components/schemas/HasSemantics" + - "$ref": "#/components/schemas/Qualifiable" + - properties: + kind: + "$ref": "#/components/schemas/ModelingKind" Event: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - "$ref": "#/components/schemas/SubmodelElement" BasicEvent: allOf: - - "$ref": "#/components/schemas/Event" - - properties: - observed: - "$ref": "#/components/schemas/Reference" - required: - - observed + - "$ref": "#/components/schemas/Event" + - properties: + observed: + "$ref": "#/components/schemas/Reference" + required: + - observed EntityType: type: string enum: - - CoManagedEntity - - SelfManagedEntity + - CoManagedEntity + - SelfManagedEntity Entity: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - properties: - statements: - type: array - items: - "$ref": "#/components/schemas/SubmodelElement" - entityType: - "$ref": "#/components/schemas/EntityType" - asset: - "$ref": "#/components/schemas/Reference" - required: - - entityType + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + statements: + type: array + items: + "$ref": "#/components/schemas/SubmodelElement" + entityType: + "$ref": "#/components/schemas/EntityType" + asset: + "$ref": "#/components/schemas/Reference" + required: + - entityType View: allOf: - - "$ref": "#/components/schemas/Referable" - - "$ref": "#/components/schemas/HasDataSpecification" - - "$ref": "#/components/schemas/HasSemantics" - - properties: - containedElements: - type: array - items: - "$ref": "#/components/schemas/Reference" + - "$ref": "#/components/schemas/Referable" + - "$ref": "#/components/schemas/HasDataSpecification" + - "$ref": "#/components/schemas/HasSemantics" + - properties: + containedElements: + type: array + items: + "$ref": "#/components/schemas/Reference" ConceptDictionary: allOf: - - "$ref": "#/components/schemas/Referable" - - "$ref": "#/components/schemas/HasDataSpecification" - - properties: - conceptDescriptions: - type: array - items: - "$ref": "#/components/schemas/Reference" + - "$ref": "#/components/schemas/Referable" + - "$ref": "#/components/schemas/HasDataSpecification" + - properties: + conceptDescriptions: + type: array + items: + "$ref": "#/components/schemas/Reference" ConceptDescription: allOf: - - "$ref": "#/components/schemas/Identifiable" - - "$ref": "#/components/schemas/HasDataSpecification" - - properties: - isCaseOf: - type: array - items: - "$ref": "#/components/schemas/Reference" + - "$ref": "#/components/schemas/Identifiable" + - "$ref": "#/components/schemas/HasDataSpecification" + - properties: + isCaseOf: + type: array + items: + "$ref": "#/components/schemas/Reference" Capability: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - "$ref": "#/components/schemas/SubmodelElement" Property: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - "$ref": "#/components/schemas/ValueObject" + - "$ref": "#/components/schemas/SubmodelElement" + - "$ref": "#/components/schemas/ValueObject" Range: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - properties: - valueType: - type: string - enum: - - anyUri - - base64Binary - - boolean - - date - - dateTime - - dateTimeStamp - - decimal - - integer - - long - - int - - short - - byte - - nonNegativeInteger - - positiveInteger - - unsignedLong - - unsignedInt - - unsignedShort - - unsignedByte - - nonPositiveInteger - - negativeInteger - - double - - duration - - dayTimeDuration - - yearMonthDuration - - float - - gDay - - gMonth - - gMonthDay - - gYear - - gYearMonth - - hexBinary - - NOTATION - - QName - - string - - normalizedString - - token - - language - - Name - - NCName - - ENTITY - - ID - - IDREF - - NMTOKEN - - time - min: - type: string - max: - type: string - required: - - valueType - - min - - max + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + min: + type: string + max: + type: string + required: + - valueType + - min + - max MultiLanguageProperty: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - properties: - value: - type: array - items: - "$ref": "#/components/schemas/LangString" - valueId: - "$ref": "#/components/schemas/Reference" + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + type: array + items: + "$ref": "#/components/schemas/LangString" + valueId: + "$ref": "#/components/schemas/Reference" File: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - properties: - value: - type: string - mimeType: - type: string - required: - - mimeType - - value + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + - value Blob: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - properties: - value: - type: string - mimeType: - type: string - required: - - mimeType - - value + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + - value ReferenceElement: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - properties: - value: - "$ref": "#/components/schemas/Reference" + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + "$ref": "#/components/schemas/Reference" SubmodelElementCollection: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - properties: - value: - type: array - items: - oneOf: - - "$ref": "#/components/schemas/Blob" - - "$ref": "#/components/schemas/File" - - "$ref": "#/components/schemas/Capability" - - "$ref": "#/components/schemas/Entity" - - "$ref": "#/components/schemas/Event" - - "$ref": "#/components/schemas/BasicEvent" - - "$ref": "#/components/schemas/MultiLanguageProperty" - - "$ref": "#/components/schemas/Operation" - - "$ref": "#/components/schemas/Property" - - "$ref": "#/components/schemas/Range" - - "$ref": "#/components/schemas/ReferenceElement" - - "$ref": "#/components/schemas/RelationshipElement" - - "$ref": "#/components/schemas/SubmodelElementCollection" - allowDuplicates: - type: boolean - ordered: - type: boolean + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + value: + type: array + items: + oneOf: + - "$ref": "#/components/schemas/Blob" + - "$ref": "#/components/schemas/File" + - "$ref": "#/components/schemas/Capability" + - "$ref": "#/components/schemas/Entity" + - "$ref": "#/components/schemas/Event" + - "$ref": "#/components/schemas/BasicEvent" + - "$ref": "#/components/schemas/MultiLanguageProperty" + - "$ref": "#/components/schemas/Operation" + - "$ref": "#/components/schemas/Property" + - "$ref": "#/components/schemas/Range" + - "$ref": "#/components/schemas/ReferenceElement" + - "$ref": "#/components/schemas/RelationshipElement" + - "$ref": "#/components/schemas/SubmodelElementCollection" + allowDuplicates: + type: boolean + ordered: + type: boolean RelationshipElement: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - properties: - first: - "$ref": "#/components/schemas/Reference" - second: - "$ref": "#/components/schemas/Reference" - required: - - first - - second + - "$ref": "#/components/schemas/SubmodelElement" + - properties: + first: + "$ref": "#/components/schemas/Reference" + second: + "$ref": "#/components/schemas/Reference" + required: + - first + - second AnnotatedRelationshipElement: allOf: - - "$ref": "#/components/schemas/RelationshipElement" - - properties: - annotation: - type: array - items: - "$ref": "#/components/schemas/Reference" + - "$ref": "#/components/schemas/RelationshipElement" + - properties: + annotation: + type: array + items: + "$ref": "#/components/schemas/Reference" Qualifier: allOf: - - "$ref": "#/components/schemas/Constraint" - - "$ref": "#/components/schemas/HasSemantics" - - "$ref": "#/components/schemas/ValueObject" - - properties: - type: - type: string - required: - - type + - "$ref": "#/components/schemas/Constraint" + - "$ref": "#/components/schemas/HasSemantics" + - "$ref": "#/components/schemas/ValueObject" + - properties: + type: + type: string + required: + - type Formula: allOf: - - "$ref": "#/components/schemas/Constraint" - - properties: - dependsOn: - type: array - items: - "$ref": "#/components/schemas/Reference" + - "$ref": "#/components/schemas/Constraint" + - properties: + dependsOn: + type: array + items: + "$ref": "#/components/schemas/Reference" Security: type: object properties: @@ -1561,27 +1561,27 @@ components: type: array items: oneOf: - - "$ref": "#/components/schemas/BlobCertificate" + - "$ref": "#/components/schemas/BlobCertificate" requiredCertificateExtension: type: array items: "$ref": "#/components/schemas/Reference" required: - - accessControlPolicyPoints + - accessControlPolicyPoints Certificate: type: object BlobCertificate: allOf: - - "$ref": "#/components/schemas/Certificate" - - properties: - blobCertificate: - "$ref": "#/components/schemas/Blob" - containedExtension: - type: array - items: - "$ref": "#/components/schemas/Reference" - lastCertificate: - type: boolean + - "$ref": "#/components/schemas/Certificate" + - properties: + blobCertificate: + "$ref": "#/components/schemas/Blob" + containedExtension: + type: array + items: + "$ref": "#/components/schemas/Reference" + lastCertificate: + type: boolean AccessControlPolicyPoints: type: object properties: @@ -1594,9 +1594,9 @@ components: policyInformationPoints: "$ref": "#/components/schemas/PolicyInformationPoints" required: - - policyAdministrationPoint - - policyDecisionPoint - - policyEnforcementPoint + - policyAdministrationPoint + - policyDecisionPoint + - policyEnforcementPoint PolicyAdministrationPoint: type: object properties: @@ -1605,7 +1605,7 @@ components: externalAccessControl: type: boolean required: - - externalAccessControl + - externalAccessControl PolicyInformationPoints: type: object properties: @@ -1616,21 +1616,21 @@ components: externalInformationPoint: type: boolean required: - - externalInformationPoint + - externalInformationPoint PolicyEnforcementPoint: type: object properties: externalPolicyEnforcementPoint: type: boolean required: - - externalPolicyEnforcementPoint + - externalPolicyEnforcementPoint PolicyDecisionPoint: type: object properties: externalPolicyDecisionPoints: type: boolean required: - - externalPolicyDecisionPoints + - externalPolicyDecisionPoints AccessControl: type: object properties: @@ -1652,20 +1652,20 @@ components: "$ref": "#/components/schemas/AccessPermissionRule" AccessPermissionRule: allOf: - - "$ref": "#/components/schemas/Referable" - - "$ref": "#/components/schemas/Qualifiable" - - properties: - targetSubjectAttributes: - type: array - items: - "$ref": "#/components/schemas/SubjectAttributes" - minItems: 1 - permissionsPerObject: - type: array - items: - "$ref": "#/components/schemas/PermissionsPerObject" - required: - - targetSubjectAttributes + - "$ref": "#/components/schemas/Referable" + - "$ref": "#/components/schemas/Qualifiable" + - properties: + targetSubjectAttributes: + type: array + items: + "$ref": "#/components/schemas/SubjectAttributes" + minItems: 1 + permissionsPerObject: + type: array + items: + "$ref": "#/components/schemas/PermissionsPerObject" + required: + - targetSubjectAttributes SubjectAttributes: type: object properties: @@ -1701,10 +1701,10 @@ components: kindOfPermission: type: string enum: - - Allow - - Deny - - NotApplicable - - Undefined + - Allow + - Deny + - NotApplicable + - Undefined required: - - permission - - kindOfPermission \ No newline at end of file + - permission + - kindOfPermission From 72b2dc12ff88480e6757ff5af5f5bfd733f8c523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 8 Sep 2020 19:33:36 +0200 Subject: [PATCH 039/157] http-api-oas: add aas submodel reference routes --- spec.yml | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/spec.yml b/spec.yml index 0e9b9f0f3..49fddae00 100644 --- a/spec.yml +++ b/spec.yml @@ -114,6 +114,64 @@ paths: $ref: "#/components/schemas/Result" tags: - Asset Administration Shell Interface + "/aas/{identifier}/submodels/{target-identifier}": + parameters: + - name: identifier + in: path + description: "Serialized identifier" + required: true + schema: + type: string + - name: target-identifier + in: path + description: "Serialized identifier of the referenced target" + required: true + schema: + type: string + get: + summary: Returns the reference specified by target-identifier + operationId: RetrieveSubmodelReference + responses: + "200": + description: Returns the reference + content: + "application/json": + schema: + $ref: "#/components/schemas/Result" + "404": + description: target-identifier is not referenced + content: + "application/json": + schema: + $ref: "#/components/schemas/Result" + default: + description: Unexpected error + content: + "application/json": + schema: + $ref: "#/components/schemas/Result" + tags: + - Asset Administration Shell Interface + delete: + summary: Deletes the reference specified by target-identifier + operationId: DeleteSubmodelReference + responses: + "201": + description: Deletes the reference + "404": + description: target-identifier is not referenced + content: + "application/json": + schema: + $ref: "#/components/schemas/Result" + default: + description: Unexpected error + content: + "application/json": + schema: + $ref: "#/components/schemas/Result" + tags: + - Asset Administration Shell Interface "/aas/{identifier}/views": parameters: - name: identifier From 0fdf5593142747405fdeef3cbcbf97fafa40dd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 24 Sep 2020 17:38:44 +0200 Subject: [PATCH 040/157] http-api-oas: remove concept dictionary routes update json schema make routes more consistent --- spec.yml | 1306 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 795 insertions(+), 511 deletions(-) diff --git a/spec.yml b/spec.yml index 49fddae00..6672912ea 100644 --- a/spec.yml +++ b/spec.yml @@ -5,7 +5,7 @@ info: description: REST API Specification for the pyI40AAS framework. Any identifier variables are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. termsOfService: None contact: - name: "Michael Hoffmeister, Manuel Sauer, Constantin Ziesche, Leon Möller" + name: "Michael Thies, Torben Miny, Leon Möller" license: name: Use under Eclipse Public License 2.0 url: "https://www.eclipse.org/legal/epl-2.0/" @@ -23,69 +23,59 @@ servers: default: v1 description: The Version of the API-Specification paths: - "/aas/{identifier}": - parameters: - - name: identifier - in: path - description: "Serialized identifier" - required: true - schema: - type: string + "/": get: summary: Retrieves the stripped Asset Administration Shell, without Submodel References, Views and Concept Dictionaries. - operationId: RetrieveAssetAdministrationShell + operationId: ReadAAS responses: "200": description: Success content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/AASResult" "404": - description: No Concept Dictionary found + description: AssetAdministrationShell not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/AASResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/AASResult" tags: - Asset Administration Shell Interface - "/aas/{identifier}/submodels": - parameters: - - name: identifier - in: path - description: "Serialized identifier" - required: true - schema: - type: string + "/submodels": get: summary: Returns a list of references to the submodels of the asset administration shell - operationId: RetrieveAllSubmodelsReferences + operationId: ReadAASSubmodelReferences responses: "200": description: Returns a list of requested Submodels or its links content: "application/json": schema: - type: array - items: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ReferenceListResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceListResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ReferenceListResult" tags: - Asset Administration Shell Interface post: summary: Adds a new Submodel reference to the Asset Administration Shell - operationId: CreateSubmodelReference + operationId: CreateAASSubmodelReference requestBody: description: The serialized Submodel reference required: true @@ -99,29 +89,29 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ReferenceResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" "409": description: Submodel reference already exists content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ReferenceResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ReferenceResult" tags: - Asset Administration Shell Interface - "/aas/{identifier}/submodels/{target-identifier}": + "/submodels/{target-identifier}": parameters: - - name: identifier - in: path - description: "Serialized identifier" - required: true - schema: - type: string - name: target-identifier in: path description: "Serialized identifier of the referenced target" @@ -130,79 +120,80 @@ paths: type: string get: summary: Returns the reference specified by target-identifier - operationId: RetrieveSubmodelReference + operationId: ReadAASSubmodelReference responses: "200": description: Returns the reference content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ReferenceResult" "404": - description: target-identifier is not referenced + description: AssetAdministrationShell not found or target-identifier is not referenced content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ReferenceResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ReferenceResult" tags: - Asset Administration Shell Interface delete: summary: Deletes the reference specified by target-identifier - operationId: DeleteSubmodelReference + operationId: DeleteAASSubmodelReference responses: - "201": - description: Deletes the reference + "200": + description: Reference deleted successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": - description: target-identifier is not referenced + description: AssetAdministrationShell not found or target-identifier is not referenced content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/BaseResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface - "/aas/{identifier}/views": - parameters: - - name: identifier - in: path - description: "Serialized identifier" - required: true - schema: - type: string + "/views": get: summary: Retrieves all Views from the Asset Administration Shell - operationId: RetrieveAllViews + operationId: ReadAASViews responses: "200": description: Returns a list of requested Views or its links content: "application/json": schema: - type: array - items: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewListResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewListResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewListResult" tags: - Asset Administration Shell Interface post: summary: Adds a new View - operationId: CreateOrUpdateView + operationId: CreateAASView requestBody: description: The serialized View required: true @@ -216,29 +207,29 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" "409": description: View with given idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface - "/aas/{identifier}/views/{view-idShort}": + "/views/{view-idShort}": parameters: - - name: identifier - in: path - description: "Serialized identifier" - required: true - schema: - type: string - name: view-idShort in: path description: "idShort of the view" @@ -247,281 +238,414 @@ paths: type: string get: summary: Retrieves a specific View from the Asset Administration Shell - operationId: RetrieveViewByIdShort + operationId: ReadAASView responses: "200": description: View retrieved successfully content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewResult" "404": - description: No View found + description: AssetAdministrationShell or View not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface put: summary: Updates a specific View from the Asset Administration Shell - operationId: UpdateViewByIdShort + operationId: UpdateAASView responses: "200": description: View updated successfully content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewResult" "404": - description: No View found + description: AssetAdministrationShell or View not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + patch: + summary: Updates a specific View from the Asset Administration Shell + operationId: PatchAASView + responses: + "200": + description: View updated successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + default: + description: Unexpected error + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface delete: summary: Deletes a specific View from the Asset Administration Shell - operationId: DeleteViewByIdShort + operationId: DeleteAASView responses: - "204": + "200": description: View deleted successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": - description: No View found + description: AssetAdministrationShell or View not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/BaseResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface - "/aas/{identifier}/conceptDictionaries": - parameters: - - name: identifier - in: path - description: "Serialized identifier" - required: true - schema: - type: string + "//": get: - summary: Retrieves all Concept Dictionaries of the Asset Administration Shell - operationId: RetrieveAllConceptDictionaries + summary: "Retrieves the stripped Submodel (without SubmodelElements and Constraints (property: qualifiers))" + operationId: ReadSubmodel responses: "200": - description: Returns a list of all Concept Dictionary + description: Success content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelResult" + "404": + description: No Submodel found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelResult" tags: - - Asset Administration Shell Interface + - Submodel Interface + "/submodelElements": + get: + summary: Retrieves all Submodel-Elements from the current Submodel + operationId: ReadSubmodelSubmodelElements + responses: + "200": + description: Returns a list of found Submodel-Elements + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "404": + description: Submodel not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + default: + description: Unexpected error + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + tags: + - Submodel Interface post: - summary: Adds a new Concept Dictionary - operationId: CreateConceptDictionary + summary: Adds a new Submodel-Element to the Submodel + operationId: CreateSubmodelSubmodelElement requestBody: - description: The serialized Concept Dictionary + description: The Submodel-Element required: true content: "application/json": schema: - $ref: "#/components/schemas/ConceptDictionary" + $ref: "#/components/schemas/SubmodelElement" responses: "201": - description: Concept Dictionary created successfully + description: SubmodelElement created successfully content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementResult" + "404": + description: Submodel not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" "409": - description: Concept Dictionary with given idShort already exists + description: Submodel-Element with given idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementResult" tags: - - Asset Administration Shell Interface - "/aas/{identifier}/conceptDictionaries/{cd-idShort}": + - Submodel Interface + "/{se-idShort1}/.../{se-idShortN}": parameters: - - name: identifier + - name: se-idShort1 in: path - description: "Serialized identifier" + description: The Submodel-Element's short id required: true schema: type: string - - name: cd-idShort + - name: se-idShortN in: path - description: The Concept Dictionary's short id + description: The Submodel-Element's short id required: true schema: type: string get: - summary: Retrieves a specific Concept Dictionary - operationId: RetrieveConceptDictionaryByIdShort + summary: Returns the (stripped) nested Submodel Element + operationId: ReadSubmodelSubmodelElement responses: "200": - description: Returns the requested Concept Dictionary + description: Returns the requested Submodel-Element content: "application/json": schema: - type: array - items: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: No Concept Dictionary found + description: Any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: - type: array - items: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementListResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" - + $ref: "#/components/schemas/SubmodelElementListResult" tags: - - Asset Administration Shell Interface + - Submodel Interface put: - summary: Update a specific Concept Dictionary - operationId: UpdateConceptDictionaryByIdShort + summary: Update a nested Submodel Element + operationId: UpdateSubmodelSubmodelElement + requestBody: + description: The Submodel-Element + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElement" responses: "200": - description: Returns the updated Concept Dictionaries + description: Returns the updated Submodel-Element content: "application/json": schema: - type: array - items: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementResult" "404": - description: No Concept Dictionary found + description: Any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementResult" tags: - - Asset Administration Shell Interface + - Submodel Interface delete: - summary: Deletes a specific Concept Dictionary - operationId: DeleteConceptDictionaryByIdShort + summary: Deletes a specific nested Submodel-Element from the Submodel + operationId: DeleteSubmodelSubmodelElement responses: - "204": - description: Concept Dictionary deleted successfully + "200": + description: Submodel-Element deleted successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": - description: No Concept Dictionary found + description: Any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/BaseResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/BaseResult" tags: - - Asset Administration Shell Interface - "/submodel/{identifier}": + - Submodel Interface + "/{se-idShort1}/.../{se-idShortN}/value": parameters: - - name: identifier + - name: se-idShort1 + in: path + description: The Submodel-Element's short id + required: true + schema: + type: string + - name: se-idShortN in: path - description: "Serialized identifier" + description: The Submodel-Element's short id required: true schema: type: string get: - summary: Retrieves the stripped Submodel (without submodel elements) - operationId: RetrieveSubmodel + summary: If the (nested) Submodel-Element is a Submodel-Element-Collection, return contained (stripped) Submodel-Elements + operationId: ReadSubmodelSubmodelElementValue responses: "200": - description: Success + description: Returns the requested Submodel-Element content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementListResult" + "400": + description: Submodel-Element exists, but is not a SubmodelElementCollection, so /value is not possible. + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: No Submodel found + description: Any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementListResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementListResult" tags: - Submodel Interface - "/submodel/{identifier}/submodelElements": + post: + summary: If the (nested) Submodel-Element is a Submodel-Element-Collection, add the Submodel-Element from the request body. + operationId: CreateSubmodelSubmodelElementValue + requestBody: + description: The Submodel-Element + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElement" + responses: + "201": + description: Submodel-Element created successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "400": + description: Submodel-Element exists, but is not a SubmodelElementCollection, so /value is not possible. + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "404": + description: Any SubmodelElement referred to by idShort[1-N] not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "409": + description: Submodel-Element with given idShort already exists. + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + default: + description: Unexpected error + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Submodel Interface + "/{se-idShort1}/.../{se-idShortN}/annotation": parameters: - - name: identifier + - name: se-idShort1 + in: path + description: The Submodel-Element's short id + required: true + schema: + type: string + - name: se-idShortN in: path - description: "Serialized identifier" + description: The Submodel-Element's short id required: true schema: type: string get: - summary: Retrieves all Submodel-Elements from the current Submodel - operationId: RetrieveAllSubmodelElements + summary: If the (nested) Submodel-Element is an AnnotatedRelationshipElement, return contained (stripped) Submodel-Elements + operationId: ReadSubmodelSubmodelElementAnnotation responses: "200": - description: Returns a list of found Submodel-Elements + description: Returns the requested Submodel-Element + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + "400": + description: Submodel-Element exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible. content: "application/json": schema: - type: array - items: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementListResult" + "404": + description: Any SubmodelElement referred to by idShort[1-N] not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementListResult" tags: - Submodel Interface post: - summary: Adds a new Submodel-Element to the Submodel - operationId: CreateSubmodelElement + summary: If the (nested) Submodel-Element is an AnnotatedRelationshipElement, add the Submodel-Element from the request body. + operationId: CreateSubmodelSubmodelElementAnnotation requestBody: description: The Submodel-Element required: true @@ -535,56 +659,80 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementResult" + "400": + description: Submodel-Element exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible. + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "404": + description: Any SubmodelElement referred to by idShort[1-N] not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" "409": - description: Submodel-Element with given idShort already exists + description: Submodel-Element with given idShort already exists. content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/submodel/{identifier}/submodelElements/{se-idShort}": + "/{se-idShort1}/.../{se-idShortN}/statement": parameters: - - name: identifier + - name: se-idShort1 in: path - description: "Serialized identifier" + description: The Submodel-Element's short id required: true schema: type: string - - name: se-idShort + - name: se-idShortN in: path description: The Submodel-Element's short id required: true schema: type: string get: - summary: Retrieves a specific Submodel-Element from the Submodel - operationId: GetSubmodelElementByIdShort + summary: If the (nested) Submodel-Element is an Entity, return contained (stripped) Submodel-Elements + operationId: ReadSubmodelSubmodelElementStatement responses: "200": description: Returns the requested Submodel-Element content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementListResult" + "400": + description: Submodel-Element exists, but is not an Entity, so /statement is not possible. + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: Submodel-Element not found + description: Any SubmodelElement referred to by idShort[1-N] not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementListResult" + default: + description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/SubmodelElementListResult" tags: - Submodel Interface - put: - summary: Update a specific Submodel Element - operationId: UpdateSubmodelElementByIdShort + post: + summary: If the (nested) Submodel-Element is an Entity, add the Submodel-Element from the request body. + operationId: CreateSubmodelSubmodelElementStatement requestBody: description: The Submodel-Element required: true @@ -592,230 +740,353 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElement" + responses: + "201": + description: Submodel-Element created successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "400": + description: Submodel-Element exists, but is not an Entity, so /statement is not possible. + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "404": + description: Any SubmodelElement referred to by idShort[1-N] not found + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "409": + description: Submodel-Element with given idShort already exists. + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + default: + description: Unexpected error + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + tags: + - Submodel Interface + "/{se-idShort1}/.../{se-idShortN}/constraints": + parameters: + - name: se-idShort1 + in: path + description: The Submodel-Element's short id + required: true + schema: + type: string + - name: se-idShortN + in: path + description: The Submodel-Element's short id + required: true + schema: + type: string + get: + summary: Retrieves all Constraints from the current Submodel + operationId: ReadSubmodelSubmodelElementConstraints responses: "200": - description: Returns the updated Submodel-Element + description: Returns a list of found Constraints content: "application/json": schema: - type: array - items: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintListResult" "404": - description: No Submodel-Element found + description: Any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintListResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintListResult" tags: - Submodel Interface - delete: - summary: Deletes a specific Submodel-Element from the Submodel - operationId: DeleteSubmodelElementByIdShort + post: + summary: Adds a new Constraint to the Submodel + operationId: CreateSubmodelSubmodelElementConstraint + requestBody: + description: The Submodel-Element + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Constraint" responses: - "204": - description: Submodel-Element deleted successfully + "201": + description: Constraint created successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" "404": - description: Submodel-Element not found + description: Any SubmodelElement referred to by idShort[1-N] not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "409": + description: "When trying to add a qualifier: Qualifier with specified type already exists" content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface - "/conceptDictionary/{identifier}": + "/{se-idShort1}/.../{se-idShortN}/constraints/{qualifier-type}": parameters: - - name: identifier + - name: se-idShort1 + in: path + description: The Submodel-Element's short id + required: true + schema: + type: string + - name: se-idShortN in: path - description: "Serialized identifier" + description: The Submodel-Element's short id + required: true + schema: + type: string + - name: qualifier-type + in: path + description: "Type of the qualifier" required: true schema: type: string get: - summary: Returns the stripped Concept Dictionary (without Concept Descriptions) - operationId: RetrieveConceptDictionary + summary: Retrieves a specific Qualifier from the submodel's constraints + operationId: ReadSubmodelSubmodelElementConstraint responses: "200": - description: Return the requested Concept Dictionary + description: Returns the qualifier content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" "404": - description: No Concept Dictionary found + description: Submodel, any SubmodelElement referred to by idShort[1-N] or qualifier not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" tags: - - Concept Dictionary Interface - "/conceptDictionary/{identifier}/conceptDescriptions": - parameters: - - name: identifier - in: path - description: "Serialized identifier" + - Submodel Interface + put: + summary: Updates an existing qualifier in the submodel + operationId: UpdateSubmodelSubmodelElementConstraint + requestBody: + description: The Submodel-Element required: true - schema: - type: string + content: + "application/json": + schema: + $ref: "#/components/schemas/Constraint" + responses: + "201": + description: Constraint created successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "404": + description: Any SubmodelElement referred to by idShort[1-N] or qualifier not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + default: + description: Unexpected error + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + delete: + summary: Deletes an existing qualifier from the submodel + operationId: DeleteSubmodelSubmodelElementConstraint + responses: + "200": + description: Constraint deleted successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: Any SubmodelElement referred to by idShort[1-N] or qualifier not found + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + default: + description: Unexpected error + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Submodel Interface + "/constraints": get: - summary: Retrieve all Concept Descriptions of the Concept Dictionary - operationId: RetrieveConceptDescriptions + summary: Retrieves all Constraints from the current Submodel + operationId: ReadSubmodelConstraints responses: "200": - description: Return Concept Descriptions + description: Returns a list of found Constraints content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintListResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintListResult" tags: - - Concept Dictionary Interface + - Submodel Interface post: - summary: Creates a new Concept Description - operationId: CreateConceptDescription + summary: Adds a new Constraint to the Submodel + operationId: CreateSubmodelConstraint requestBody: - description: The serialized Concept Description + description: The Submodel-Element required: true content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/Constraint" responses: "201": - description: Concept Description created successfully + description: Constraint created successfully content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" "409": - description: Concept Description already exists + description: "When trying to add a qualifier: Qualifier with specified type already exists" content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" tags: - - Concept Dictionary Interface - "/conceptDictionary/{identifier}/conceptDescriptions/{cd-idShort}": + - Submodel Interface + "/constraints/{qualifier-type}": parameters: - - name: identifier - in: path - description: "Serialized identifier" - required: true - schema: - type: string - - name: cd-idShort + - name: qualifier-type in: path - description: The Concept Description's short id + description: "Type of the qualifier" required: true schema: type: string get: - summary: Retrieves a specific Concept Description - operationId: RetrieveConceptDescriptionByIdShort + summary: Retrieves a specific Qualifier from the submodel's constraints + operationId: ReadSubmodelConstraint responses: "200": - description: Return the requested Concept Description + description: Returns the qualifier content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" "404": - description: No Concept Description found + description: Submodel or Constraint not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" tags: - - Concept Dictionary Interface + - Submodel Interface put: - summary: Updates a specific Concept Description - operationId: UpdateConceptDescriptionByIdShort + summary: Updates an existing qualifier in the submodel + operationId: UpdateSubmodelConstraint requestBody: - description: The Concept Description + description: The Submodel-Element required: true content: "application/json": schema: - $ref: "#/components/schemas/ConceptDescription" + $ref: "#/components/schemas/ConstraintResult" responses: - "200": - description: Concept Description updated successfully + "201": + description: Constraint created successfully content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" "404": - description: No Concept Description found + description: Submodel or Constraint not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/ConstraintResult" tags: - - Concept Dictionary Interface + - Submodel Interface delete: - summary: Deletes a specific Concept Description - operationId: DeleteConceptDescriptionByIdShort + summary: Deletes an existing qualifier from the submodel + operationId: DeleteSubmodelConstraint responses: - "204": - description: Concept Description deleted successfully + "200": + description: Constraint deleted successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": - description: No Concept Description found + description: Submodel or Constraint not found content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/BaseResult" default: description: Unexpected error content: "application/json": schema: - $ref: "#/components/schemas/Result" + $ref: "#/components/schemas/BaseResult" tags: - - Concept Dictionary Interface + - Submodel Interface components: schemas: - Result: + BaseResult: type: object properties: success: @@ -843,89 +1114,75 @@ components: data: nullable: true readOnly: true - oneOf: - - $ref: "#/components/schemas/Referable" - - type: array - items: - $ref: "#/components/schemas/Referable" - - $ref: "#/components/schemas/Reference" - - type: array + type: object + AASResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedAssetAdministrationShell" + ReferenceResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/Reference" + ReferenceListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array items: $ref: "#/components/schemas/Reference" - OperationRequest: - type: object - properties: - requestId: - type: string - callbackUrls: - type: object - properties: - failedUrl: - type: string - successUrl: - type: string - params: - type: array - items: - $ref: "#/components/schemas/OperationVariable" - EventSubscription: - type: object - properties: - clientId: - type: string - endpoint: - $ref: "#/components/schemas/Endpoint" - AssetAdministrationShellDescriptor: - type: object - properties: - idShort: - type: string - identification: - $ref: "#/components/schemas/Identifier" - administration: - $ref: "#/components/schemas/AdministrativeInformation" - description: - type: array - items: - $ref: "#/components/schemas/LangString" - asset: - $ref: "#/components/schemas/Asset" - endpoints: - type: array - items: - $ref: "#/components/schemas/Endpoint" - submodelDescriptors: - type: array - items: - $ref: "#/components/schemas/SubmodelDescriptor" - SubmodelDescriptor: - type: object - properties: - idShort: - type: string - identification: - $ref: "#/components/schemas/Identifier" - administration: - $ref: "#/components/schemas/AdministrativeInformation" - description: - type: array - items: - $ref: "#/components/schemas/LangString" - semanticId: - $ref: "#/components/schemas/Reference" - endpoints: - type: array - items: - $ref: "#/components/schemas/Endpoint" - Endpoint: - type: object - properties: - address: - type: string - type: - type: string - parameters: - type: object + ViewResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/View" + ViewListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/View" + SubmodelResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedSubmodel" + SubmodelElementResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedSubmodelElement" + SubmodelElementListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/StrippedSubmodelElement" + ConstraintResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/Constraint" + ConstraintListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/Constraint" Referable: type: object properties: @@ -936,22 +1193,22 @@ components: description: type: array items: - "$ref": "#/components/schemas/LangString" + $ref: "#/components/schemas/LangString" parent: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" modelType: - "$ref": "#/components/schemas/ModelType" + $ref: "#/components/schemas/ModelType" required: - idShort - modelType Identifiable: allOf: - - "$ref": "#/components/schemas/Referable" + - $ref: "#/components/schemas/Referable" - properties: identification: - "$ref": "#/components/schemas/Identifier" + $ref: "#/components/schemas/Identifier" administration: - "$ref": "#/components/schemas/AdministrativeInformation" + $ref: "#/components/schemas/AdministrativeInformation" required: - identification Qualifiable: @@ -960,53 +1217,61 @@ components: qualifiers: type: array items: - "$ref": "#/components/schemas/Constraint" + $ref: "#/components/schemas/Constraint" HasSemantics: type: object properties: semanticId: - "$ref": "#/components/schemas/Reference" - required: - - semanticId + $ref: "#/components/schemas/Reference" HasDataSpecification: type: object properties: embeddedDataSpecifications: type: array items: - "$ref": "#/components/schemas/EmbeddedDataSpecification" + $ref: "#/components/schemas/EmbeddedDataSpecification" AssetAdministrationShell: allOf: - - "$ref": "#/components/schemas/Identifiable" - - "$ref": "#/components/schemas/HasDataSpecification" + - $ref: "#/components/schemas/Identifiable" + - $ref: "#/components/schemas/HasDataSpecification" - properties: derivedFrom: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" asset: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" submodels: type: array items: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" views: type: array items: - "$ref": "#/components/schemas/View" + $ref: "#/components/schemas/View" conceptDictionaries: type: array items: - "$ref": "#/components/schemas/ConceptDictionary" + $ref: "#/components/schemas/ConceptDictionary" security: - "$ref": "#/components/schemas/Security" + $ref: "#/components/schemas/Security" required: - asset + StrippedAssetAdministrationShell: + allOf: + - $ref: "#/components/schemas/AssetAdministrationShell" + - properties: + views: + not: {} + submodels: + not: {} + conceptDictionaries: + not: {} Identifier: type: object properties: id: type: string idType: - "$ref": "#/components/schemas/KeyType" + $ref: "#/components/schemas/KeyType" required: - id - idType @@ -1041,16 +1306,16 @@ components: keys: type: array items: - "$ref": "#/components/schemas/Key" + $ref: "#/components/schemas/Key" required: - keys Key: type: object properties: type: - "$ref": "#/components/schemas/KeyElements" + $ref: "#/components/schemas/KeyElements" idType: - "$ref": "#/components/schemas/KeyType" + $ref: "#/components/schemas/KeyType" value: type: string local: @@ -1123,23 +1388,23 @@ components: type: object properties: name: - "$ref": "#/components/schemas/ModelTypes" + $ref: "#/components/schemas/ModelTypes" required: - name EmbeddedDataSpecification: type: object properties: dataSpecification: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" dataSpecificationContent: - "$ref": "#/components/schemas/DataSpecificationContent" + $ref: "#/components/schemas/DataSpecificationContent" required: - dataSpecification - dataSpecificationContent DataSpecificationContent: oneOf: - - "$ref": "#/components/schemas/DataSpecificationIEC61360Content" - - "$ref": "#/components/schemas/DataSpecificationPhysicalUnitContent" + - $ref: "#/components/schemas/DataSpecificationIEC61360Content" + - $ref: "#/components/schemas/DataSpecificationPhysicalUnitContent" DataSpecificationPhysicalUnitContent: type: object properties: @@ -1150,7 +1415,7 @@ components: definition: type: array items: - "$ref": "#/components/schemas/LangString" + $ref: "#/components/schemas/LangString" siNotation: type: string siName: @@ -1177,7 +1442,7 @@ components: - definition DataSpecificationIEC61360Content: allOf: - - "$ref": "#/components/schemas/ValueObject" + - $ref: "#/components/schemas/ValueObject" - type: object properties: dataType: @@ -1194,18 +1459,21 @@ components: - RATIONAL_MEASURE - TIME - TIMESTAMP + - INTEGER_COUNT + - INTEGER_MEASURE + - INTEGER_CURRENCY definition: type: array items: - "$ref": "#/components/schemas/LangString" + $ref: "#/components/schemas/LangString" preferredName: type: array items: - "$ref": "#/components/schemas/LangString" + $ref: "#/components/schemas/LangString" shortName: type: array items: - "$ref": "#/components/schemas/LangString" + $ref: "#/components/schemas/LangString" sourceOfDefinition: type: string symbol: @@ -1213,15 +1481,15 @@ components: unit: type: string unitId: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" valueFormat: type: string valueList: - "$ref": "#/components/schemas/ValueList" + $ref: "#/components/schemas/ValueList" levelType: type: array items: - "$ref": "#/components/schemas/LevelType" + $ref: "#/components/schemas/LevelType" required: - preferredName LevelType: @@ -1238,19 +1506,19 @@ components: type: array minItems: 1 items: - "$ref": "#/components/schemas/ValueReferencePairType" + $ref: "#/components/schemas/ValueReferencePairType" required: - valueReferencePairTypes ValueReferencePairType: allOf: - - "$ref": "#/components/schemas/ValueObject" + - $ref: "#/components/schemas/ValueObject" ValueObject: type: object properties: value: type: string valueId: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" valueType: type: string enum: @@ -1300,15 +1568,15 @@ components: - time Asset: allOf: - - "$ref": "#/components/schemas/Identifiable" - - "$ref": "#/components/schemas/HasDataSpecification" + - $ref: "#/components/schemas/Identifiable" + - $ref: "#/components/schemas/HasDataSpecification" - properties: kind: - "$ref": "#/components/schemas/AssetKind" + $ref: "#/components/schemas/AssetKind" assetIdentificationModel: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" billOfMaterial: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" required: - kind AssetKind: @@ -1323,78 +1591,92 @@ components: - Instance Submodel: allOf: - - "$ref": "#/components/schemas/Identifiable" - - "$ref": "#/components/schemas/HasDataSpecification" - - "$ref": "#/components/schemas/Qualifiable" - - "$ref": "#/components/schemas/HasSemantics" + - $ref: "#/components/schemas/Identifiable" + - $ref: "#/components/schemas/HasDataSpecification" + - $ref: "#/components/schemas/Qualifiable" + - $ref: "#/components/schemas/HasSemantics" - properties: kind: - "$ref": "#/components/schemas/ModelingKind" + $ref: "#/components/schemas/ModelingKind" submodelElements: type: array items: - "$ref": "#/components/schemas/SubmodelElement" + $ref: "#/components/schemas/SubmodelElement" + StrippedSubmodel: + allOf: + - $ref: "#/components/schemas/Submodel" + - properties: + submodelElements: + not: {} + qualifiers: + not: {} Constraint: type: object properties: modelType: - "$ref": "#/components/schemas/ModelType" + $ref: "#/components/schemas/ModelType" required: - modelType Operation: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" - properties: inputVariable: type: array items: - "$ref": "#/components/schemas/OperationVariable" + $ref: "#/components/schemas/OperationVariable" outputVariable: type: array items: - "$ref": "#/components/schemas/OperationVariable" + $ref: "#/components/schemas/OperationVariable" inoutputVariable: type: array items: - "$ref": "#/components/schemas/OperationVariable" + $ref: "#/components/schemas/OperationVariable" OperationVariable: type: object properties: value: oneOf: - - "$ref": "#/components/schemas/Blob" - - "$ref": "#/components/schemas/File" - - "$ref": "#/components/schemas/Capability" - - "$ref": "#/components/schemas/Entity" - - "$ref": "#/components/schemas/Event" - - "$ref": "#/components/schemas/BasicEvent" - - "$ref": "#/components/schemas/MultiLanguageProperty" - - "$ref": "#/components/schemas/Operation" - - "$ref": "#/components/schemas/Property" - - "$ref": "#/components/schemas/Range" - - "$ref": "#/components/schemas/ReferenceElement" - - "$ref": "#/components/schemas/RelationshipElement" - - "$ref": "#/components/schemas/SubmodelElementCollection" + - $ref: "#/components/schemas/Blob" + - $ref: "#/components/schemas/File" + - $ref: "#/components/schemas/Capability" + - $ref: "#/components/schemas/Entity" + - $ref: "#/components/schemas/Event" + - $ref: "#/components/schemas/BasicEvent" + - $ref: "#/components/schemas/MultiLanguageProperty" + - $ref: "#/components/schemas/Operation" + - $ref: "#/components/schemas/Property" + - $ref: "#/components/schemas/Range" + - $ref: "#/components/schemas/ReferenceElement" + - $ref: "#/components/schemas/RelationshipElement" + - $ref: "#/components/schemas/SubmodelElementCollection" required: - value SubmodelElement: allOf: - - "$ref": "#/components/schemas/Referable" - - "$ref": "#/components/schemas/HasDataSpecification" - - "$ref": "#/components/schemas/HasSemantics" - - "$ref": "#/components/schemas/Qualifiable" + - $ref: "#/components/schemas/Referable" + - $ref: "#/components/schemas/HasDataSpecification" + - $ref: "#/components/schemas/HasSemantics" + - $ref: "#/components/schemas/Qualifiable" - properties: kind: - "$ref": "#/components/schemas/ModelingKind" + $ref: "#/components/schemas/ModelingKind" + StrippedSubmodelElement: + allOf: + - $ref: "#/components/schemas/SubmodelElement" + - properties: + qualifiers: + not: {} Event: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" BasicEvent: allOf: - - "$ref": "#/components/schemas/Event" + - $ref: "#/components/schemas/Event" - properties: observed: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" required: - observed EntityType: @@ -1404,56 +1686,56 @@ components: - SelfManagedEntity Entity: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" - properties: statements: type: array items: - "$ref": "#/components/schemas/SubmodelElement" + $ref: "#/components/schemas/SubmodelElement" entityType: - "$ref": "#/components/schemas/EntityType" + $ref: "#/components/schemas/EntityType" asset: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" required: - entityType View: allOf: - - "$ref": "#/components/schemas/Referable" - - "$ref": "#/components/schemas/HasDataSpecification" - - "$ref": "#/components/schemas/HasSemantics" + - $ref: "#/components/schemas/Referable" + - $ref: "#/components/schemas/HasDataSpecification" + - $ref: "#/components/schemas/HasSemantics" - properties: containedElements: type: array items: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" ConceptDictionary: allOf: - - "$ref": "#/components/schemas/Referable" - - "$ref": "#/components/schemas/HasDataSpecification" + - $ref: "#/components/schemas/Referable" + - $ref: "#/components/schemas/HasDataSpecification" - properties: conceptDescriptions: type: array items: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" ConceptDescription: allOf: - - "$ref": "#/components/schemas/Identifiable" - - "$ref": "#/components/schemas/HasDataSpecification" + - $ref: "#/components/schemas/Identifiable" + - $ref: "#/components/schemas/HasDataSpecification" - properties: isCaseOf: type: array items: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" Capability: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" Property: allOf: - - "$ref": "#/components/schemas/SubmodelElement" - - "$ref": "#/components/schemas/ValueObject" + - $ref: "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/ValueObject" Range: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" - properties: valueType: type: string @@ -1508,21 +1790,19 @@ components: type: string required: - valueType - - min - - max MultiLanguageProperty: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" - properties: value: type: array items: - "$ref": "#/components/schemas/LangString" + $ref: "#/components/schemas/LangString" valueId: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" File: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" - properties: value: type: string @@ -1530,10 +1810,9 @@ components: type: string required: - mimeType - - value Blob: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" - properties: value: type: string @@ -1541,62 +1820,67 @@ components: type: string required: - mimeType - - value ReferenceElement: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" - properties: value: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" SubmodelElementCollection: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" - properties: value: type: array items: oneOf: - - "$ref": "#/components/schemas/Blob" - - "$ref": "#/components/schemas/File" - - "$ref": "#/components/schemas/Capability" - - "$ref": "#/components/schemas/Entity" - - "$ref": "#/components/schemas/Event" - - "$ref": "#/components/schemas/BasicEvent" - - "$ref": "#/components/schemas/MultiLanguageProperty" - - "$ref": "#/components/schemas/Operation" - - "$ref": "#/components/schemas/Property" - - "$ref": "#/components/schemas/Range" - - "$ref": "#/components/schemas/ReferenceElement" - - "$ref": "#/components/schemas/RelationshipElement" - - "$ref": "#/components/schemas/SubmodelElementCollection" + - $ref: "#/components/schemas/Blob" + - $ref: "#/components/schemas/File" + - $ref: "#/components/schemas/Capability" + - $ref: "#/components/schemas/Entity" + - $ref: "#/components/schemas/Event" + - $ref: "#/components/schemas/BasicEvent" + - $ref: "#/components/schemas/MultiLanguageProperty" + - $ref: "#/components/schemas/Operation" + - $ref: "#/components/schemas/Property" + - $ref: "#/components/schemas/Range" + - $ref: "#/components/schemas/ReferenceElement" + - $ref: "#/components/schemas/RelationshipElement" + - $ref: "#/components/schemas/SubmodelElementCollection" allowDuplicates: type: boolean ordered: type: boolean RelationshipElement: allOf: - - "$ref": "#/components/schemas/SubmodelElement" + - $ref: "#/components/schemas/SubmodelElement" - properties: first: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" second: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" required: - first - second AnnotatedRelationshipElement: allOf: - - "$ref": "#/components/schemas/RelationshipElement" + - $ref: "#/components/schemas/RelationshipElement" - properties: annotation: type: array items: - "$ref": "#/components/schemas/Reference" + oneOf: + - $ref: "#/components/schemas/Blob" + - $ref: "#/components/schemas/File" + - $ref: "#/components/schemas/MultiLanguageProperty" + - $ref: "#/components/schemas/Property" + - $ref: "#/components/schemas/Range" + - $ref: "#/components/schemas/ReferenceElement" Qualifier: allOf: - - "$ref": "#/components/schemas/Constraint" - - "$ref": "#/components/schemas/HasSemantics" - - "$ref": "#/components/schemas/ValueObject" + - $ref: "#/components/schemas/Constraint" + - $ref: "#/components/schemas/HasSemantics" + - $ref: "#/components/schemas/ValueObject" - properties: type: type: string @@ -1604,53 +1888,53 @@ components: - type Formula: allOf: - - "$ref": "#/components/schemas/Constraint" + - $ref: "#/components/schemas/Constraint" - properties: dependsOn: type: array items: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" Security: type: object properties: accessControlPolicyPoints: - "$ref": "#/components/schemas/AccessControlPolicyPoints" + $ref: "#/components/schemas/AccessControlPolicyPoints" certificate: type: array items: oneOf: - - "$ref": "#/components/schemas/BlobCertificate" + - $ref: "#/components/schemas/BlobCertificate" requiredCertificateExtension: type: array items: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" required: - accessControlPolicyPoints Certificate: type: object BlobCertificate: allOf: - - "$ref": "#/components/schemas/Certificate" + - $ref: "#/components/schemas/Certificate" - properties: blobCertificate: - "$ref": "#/components/schemas/Blob" + $ref: "#/components/schemas/Blob" containedExtension: type: array items: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" lastCertificate: type: boolean AccessControlPolicyPoints: type: object properties: policyAdministrationPoint: - "$ref": "#/components/schemas/PolicyAdministrationPoint" + $ref: "#/components/schemas/PolicyAdministrationPoint" policyDecisionPoint: - "$ref": "#/components/schemas/PolicyDecisionPoint" + $ref: "#/components/schemas/PolicyDecisionPoint" policyEnforcementPoint: - "$ref": "#/components/schemas/PolicyEnforcementPoint" + $ref: "#/components/schemas/PolicyEnforcementPoint" policyInformationPoints: - "$ref": "#/components/schemas/PolicyInformationPoints" + $ref: "#/components/schemas/PolicyInformationPoints" required: - policyAdministrationPoint - policyDecisionPoint @@ -1659,7 +1943,7 @@ components: type: object properties: localAccessControl: - "$ref": "#/components/schemas/AccessControl" + $ref: "#/components/schemas/AccessControl" externalAccessControl: type: boolean required: @@ -1670,7 +1954,7 @@ components: internalInformationPoint: type: array items: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" externalInformationPoint: type: boolean required: @@ -1693,35 +1977,35 @@ components: type: object properties: selectableSubjectAttributes: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" defaultSubjectAttributes: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" selectablePermissions: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" defaultPermissions: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" selectableEnvironmentAttributes: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" defaultEnvironmentAttributes: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" accessPermissionRule: type: array items: - "$ref": "#/components/schemas/AccessPermissionRule" + $ref: "#/components/schemas/AccessPermissionRule" AccessPermissionRule: allOf: - - "$ref": "#/components/schemas/Referable" - - "$ref": "#/components/schemas/Qualifiable" + - $ref: "#/components/schemas/Referable" + - $ref: "#/components/schemas/Qualifiable" - properties: targetSubjectAttributes: type: array items: - "$ref": "#/components/schemas/SubjectAttributes" + $ref: "#/components/schemas/SubjectAttributes" minItems: 1 permissionsPerObject: type: array items: - "$ref": "#/components/schemas/PermissionsPerObject" + $ref: "#/components/schemas/PermissionsPerObject" required: - targetSubjectAttributes SubjectAttributes: @@ -1730,32 +2014,32 @@ components: subjectAttributes: type: array items: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" minItems: 1 PermissionsPerObject: type: object properties: object: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" targetObjectAttributes: - "$ref": "#/components/schemas/ObjectAttributes" + $ref: "#/components/schemas/ObjectAttributes" permission: type: array items: - "$ref": "#/components/schemas/Permission" + $ref: "#/components/schemas/Permission" ObjectAttributes: type: object properties: objectAttribute: type: array items: - "$ref": "#/components/schemas/Property" + $ref: "#/components/schemas/Property" minItems: 1 Permission: type: object properties: permission: - "$ref": "#/components/schemas/Reference" + $ref: "#/components/schemas/Reference" kindOfPermission: type: string enum: From 83ababd3499be9af4a55f6bf78fde92080d3f3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 29 Sep 2020 17:53:43 +0200 Subject: [PATCH 041/157] http-api-oas: cleanup descriptions remove default responses smaller fixes --- spec.yml | 657 +++++++++++++++++++++---------------------------------- 1 file changed, 247 insertions(+), 410 deletions(-) diff --git a/spec.yml b/spec.yml index 6672912ea..7498d9666 100644 --- a/spec.yml +++ b/spec.yml @@ -2,16 +2,25 @@ openapi: 3.0.0 info: version: "1" title: pyI40AAS REST API - description: REST API Specification for the pyI40AAS framework. Any identifier variables are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. - termsOfService: None + description: "REST API Specification for the pyI40AAS framework. + + + Any identifier variables are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. + + + This specification contains one exemplary `PATCH` route. In the future there should be a `PATCH` route for every `PUT` route, where it makes sense. + + + In our implementation the AAS Interface will be available at `/aas/{identifier}` and the Submodel Interface will be available at `/submodel/{identifier}`." contact: name: "Michael Thies, Torben Miny, Leon Möller" - license: - name: Use under Eclipse Public License 2.0 - url: "https://www.eclipse.org/legal/epl-2.0/" +# TODO: which license? +# license: +# name: Use under Eclipse Public License 2.0 +# url: "https://www.eclipse.org/legal/epl-2.0/" servers: - url: http://{authority}/{basePath}/{api-version} - description: "This is the Server to access the Asset Administration Shell" + description: This is the Server to access the Asset Administration Shell variables: authority: default: localhost:8080 @@ -25,7 +34,7 @@ servers: paths: "/": get: - summary: Retrieves the stripped Asset Administration Shell, without Submodel References, Views and Concept Dictionaries. + summary: Retrieves the stripped AssetAdministrationShell, without Submodel-References and Views. operationId: ReadAAS responses: "200": @@ -40,21 +49,15 @@ paths: "application/json": schema: $ref: "#/components/schemas/AASResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/AASResult" tags: - Asset Administration Shell Interface "/submodels": get: - summary: Returns a list of references to the submodels of the asset administration shell + summary: Returns all Submodel-References of the AssetAdministrationShell operationId: ReadAASSubmodelReferences responses: "200": - description: Returns a list of requested Submodels or its links + description: Success content: "application/json": schema: @@ -65,19 +68,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ReferenceListResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceListResult" tags: - Asset Administration Shell Interface post: - summary: Adds a new Submodel reference to the Asset Administration Shell + summary: Adds a new Submodel-Reference to the AssetAdministrationShell operationId: CreateAASSubmodelReference requestBody: - description: The serialized Submodel reference + description: The Submodel-Reference to create required: true content: "application/json": @@ -85,7 +82,7 @@ paths: $ref: "#/components/schemas/Reference" responses: "201": - description: Submodel reference created successfully + description: Success content: "application/json": schema: @@ -97,45 +94,33 @@ paths: schema: $ref: "#/components/schemas/ReferenceResult" "409": - description: Submodel reference already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - default: - description: Unexpected error + description: Submodel-Reference already exists content: "application/json": schema: $ref: "#/components/schemas/ReferenceResult" tags: - Asset Administration Shell Interface - "/submodels/{target-identifier}": + "/submodels/{submodel-identifier}": parameters: - - name: target-identifier + - name: submodel-identifier in: path - description: "Serialized identifier of the referenced target" + description: The Identifier of the referenced Submodel required: true schema: type: string get: - summary: Returns the reference specified by target-identifier + summary: Returns the Reference specified by submodel-identifier operationId: ReadAASSubmodelReference responses: "200": - description: Returns the reference + description: Success content: "application/json": schema: $ref: "#/components/schemas/ReferenceResult" "404": - description: AssetAdministrationShell not found or target-identifier is not referenced - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - default: - description: Unexpected error + description: AssetAdministrationShell not found or the specified Submodel is not referenced content: "application/json": schema: @@ -143,11 +128,11 @@ paths: tags: - Asset Administration Shell Interface delete: - summary: Deletes the reference specified by target-identifier + summary: Deletes the Reference specified by submodel-identifier operationId: DeleteAASSubmodelReference responses: "200": - description: Reference deleted successfully + description: Success content: "application/json": schema: @@ -158,21 +143,15 @@ paths: "application/json": schema: $ref: "#/components/schemas/BaseResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface "/views": get: - summary: Retrieves all Views from the Asset Administration Shell + summary: Returns all Views of the AssetAdministrationShell operationId: ReadAASViews responses: "200": - description: Returns a list of requested Views or its links + description: Success content: "application/json": schema: @@ -183,19 +162,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewListResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewListResult" tags: - Asset Administration Shell Interface post: - summary: Adds a new View + summary: Adds a new View to the AssetAdministrationShell operationId: CreateAASView requestBody: - description: The serialized View + description: The View to create required: true content: "application/json": @@ -203,7 +176,7 @@ paths: $ref: "#/components/schemas/View" responses: "201": - description: View created successfully + description: Success content: "application/json": schema: @@ -215,13 +188,7 @@ paths: schema: $ref: "#/components/schemas/ViewResult" "409": - description: View with given idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - default: - description: Unexpected error + description: View with same idShort already exists content: "application/json": schema: @@ -232,16 +199,16 @@ paths: parameters: - name: view-idShort in: path - description: "idShort of the view" + description: The idShort of the View required: true schema: type: string get: - summary: Retrieves a specific View from the Asset Administration Shell + summary: Returns a specific View of the AssetAdministrationShell operationId: ReadAASView responses: "200": - description: View retrieved successfully + description: Success content: "application/json": schema: @@ -252,20 +219,21 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface put: - summary: Updates a specific View from the Asset Administration Shell + summary: Updates a specific View of the AssetAdministrationShell operationId: UpdateAASView + requestBody: + description: The View used to overwrite the existing View + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" responses: "200": - description: View updated successfully + description: Success content: "application/json": schema: @@ -276,20 +244,21 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface patch: - summary: Updates a specific View from the Asset Administration Shell + summary: Updates a specific View of the AssetAdministrationShell by only providing properties that should be changed operationId: PatchAASView + requestBody: + description: The (partial) View used to update the existing View + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" responses: "200": - description: View updated successfully + description: Success content: "application/json": schema: @@ -300,12 +269,6 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface delete: @@ -313,7 +276,7 @@ paths: operationId: DeleteAASView responses: "200": - description: View deleted successfully + description: Success content: "application/json": schema: @@ -324,17 +287,11 @@ paths: "application/json": schema: $ref: "#/components/schemas/BaseResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface "//": get: - summary: "Retrieves the stripped Submodel (without SubmodelElements and Constraints (property: qualifiers))" + summary: "Returns the stripped Submodel (without SubmodelElements and Constraints (property: qualifiers))" operationId: ReadSubmodel responses: "200": @@ -349,33 +306,140 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelResult" - default: - description: Unexpected error + tags: + - Submodel Interface + "/constraints": + get: + summary: Returns all Constraints of the current Submodel + operationId: ReadSubmodelConstraints + responses: + "200": + description: Success content: "application/json": schema: - $ref: "#/components/schemas/SubmodelResult" + $ref: "#/components/schemas/ConstraintListResult" + "404": + description: Submodel not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintListResult" tags: - Submodel Interface - "/submodelElements": + post: + summary: Adds a new Constraint to the Submodel + operationId: CreateSubmodelConstraint + requestBody: + description: The Constraint to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Constraint" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "404": + description: Submodel not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "409": + description: "When trying to add a qualifier: Qualifier with same type already exists" + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + "/constraints/{qualifier-type}": + parameters: + - name: qualifier-type + in: path + description: The type of the Qualifier + required: true + schema: + type: string get: - summary: Retrieves all Submodel-Elements from the current Submodel - operationId: ReadSubmodelSubmodelElements + summary: Retrieves a specific Qualifier of the Submodel's constraints (Formulas cannot be referred to yet) + operationId: ReadSubmodelConstraint responses: "200": - description: Returns a list of found Submodel-Elements + description: Success content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/ConstraintResult" "404": - description: Submodel not found + description: Submodel or Constraint not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + put: + summary: Updates an existing Qualifier in the Submodel (Formulas cannot be referred to yet) + operationId: UpdateSubmodelConstraint + requestBody: + description: The Qualifier used to overwrite the existing Qualifier + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Qualifier" + responses: + "201": + description: Constraint created successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "404": + description: Submodel or Constraint not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + tags: + - Submodel Interface + delete: + summary: Deletes an existing Qualifier from the Submodel (Formulas cannot be referred to yet) + operationId: DeleteSubmodelConstraint + responses: + "200": + description: Constraint deleted successfully + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: Submodel or Constraint not found + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Submodel Interface + "/submodelElements": + get: + summary: Returns all SubmodelElements of the current Submodel + operationId: ReadSubmodelSubmodelElements + responses: + "200": + description: Success content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" - default: - description: Unexpected error + "404": + description: Submodel not found content: "application/json": schema: @@ -383,10 +447,10 @@ paths: tags: - Submodel Interface post: - summary: Adds a new Submodel-Element to the Submodel + summary: Adds a new SubmodelElement to the Submodel operationId: CreateSubmodelSubmodelElement requestBody: - description: The Submodel-Element + description: The SubmodelElement to create required: true content: "application/json": @@ -394,7 +458,7 @@ paths: $ref: "#/components/schemas/SubmodelElement" responses: "201": - description: SubmodelElement created successfully + description: Success content: "application/json": schema: @@ -406,13 +470,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "409": - description: Submodel-Element with given idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - default: - description: Unexpected error + description: SubmodelElement with same idShort already exists content: "application/json": schema: @@ -423,34 +481,28 @@ paths: parameters: - name: se-idShort1 in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string - name: se-idShortN in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string get: - summary: Returns the (stripped) nested Submodel Element + summary: Returns the (stripped) (nested) SubmodelElement operationId: ReadSubmodelSubmodelElement responses: "200": - description: Returns the requested Submodel-Element + description: Success content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementListResult" - default: - description: Unexpected error + description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: @@ -458,10 +510,10 @@ paths: tags: - Submodel Interface put: - summary: Update a nested Submodel Element + summary: Updates a nested SubmodelElement operationId: UpdateSubmodelSubmodelElement requestBody: - description: The Submodel-Element + description: The SubmodelElement used to overwrite the existing SubmodelElement required: true content: "application/json": @@ -469,19 +521,13 @@ paths: $ref: "#/components/schemas/SubmodelElement" responses: "200": - description: Returns the updated Submodel-Element + description: Returns the updated SubmodelElement content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - default: - description: Unexpected error + description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: @@ -489,23 +535,17 @@ paths: tags: - Submodel Interface delete: - summary: Deletes a specific nested Submodel-Element from the Submodel + summary: Deletes a specific (nested) SubmodelElement from the Submodel operationId: DeleteSubmodelSubmodelElement responses: "200": - description: Submodel-Element deleted successfully + description: Success content: "application/json": schema: $ref: "#/components/schemas/BaseResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - default: - description: Unexpected error + description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: @@ -516,40 +556,34 @@ paths: parameters: - name: se-idShort1 in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string - name: se-idShortN in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string get: - summary: If the (nested) Submodel-Element is a Submodel-Element-Collection, return contained (stripped) Submodel-Elements + summary: If the (nested) SubmodelElement is a SubmodelElementCollection, return contained (stripped) SubmodelElements operationId: ReadSubmodelSubmodelElementValue responses: "200": - description: Returns the requested Submodel-Element + description: Success content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" "400": - description: Submodel-Element exists, but is not a SubmodelElementCollection, so /value is not possible. + description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementListResult" - default: - description: Unexpected error + description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: @@ -557,10 +591,10 @@ paths: tags: - Submodel Interface post: - summary: If the (nested) Submodel-Element is a Submodel-Element-Collection, add the Submodel-Element from the request body. + summary: If the (nested) SubmodelElement is a SubmodelElementCollection, add a SubmodelElement to its value operationId: CreateSubmodelSubmodelElementValue requestBody: - description: The Submodel-Element + description: The SubmodelElement to create required: true content: "application/json": @@ -568,63 +602,57 @@ paths: $ref: "#/components/schemas/SubmodelElement" responses: "201": - description: Submodel-Element created successfully + description: Success content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" "400": - description: Submodel-Element exists, but is not a SubmodelElementCollection, so /value is not possible. + description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" "409": - description: Submodel-Element with given idShort already exists. + description: SubmodelElement with same idShort already exists content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{se-idShort1}/.../{se-idShortN}/annotation": parameters: - name: se-idShort1 in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string - name: se-idShortN in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string get: - summary: If the (nested) Submodel-Element is an AnnotatedRelationshipElement, return contained (stripped) Submodel-Elements + summary: If the (nested) SubmodelElement is an AnnotatedRelationshipElement, return contained (stripped) SubmodelElements operationId: ReadSubmodelSubmodelElementAnnotation responses: "200": - description: Returns the requested Submodel-Element + description: Success content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" "400": - description: Submodel-Element exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible. + description: SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible content: "application/json": schema: @@ -635,19 +663,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementListResult" tags: - Submodel Interface post: - summary: If the (nested) Submodel-Element is an AnnotatedRelationshipElement, add the Submodel-Element from the request body. + summary: If the (nested) SubmodelElement is an AnnotatedRelationshipElement, add a SubmodelElement to its annotation operationId: CreateSubmodelSubmodelElementAnnotation requestBody: - description: The Submodel-Element + description: The SubmodelElement to create required: true content: "application/json": @@ -655,13 +677,13 @@ paths: $ref: "#/components/schemas/SubmodelElement" responses: "201": - description: Submodel-Element created successfully + description: Success content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" "400": - description: Submodel-Element exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible. + description: SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible content: "application/json": schema: @@ -673,13 +695,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "409": - description: Submodel-Element with given idShort already exists. - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - default: - description: Unexpected error + description: SubmodelElement with given idShort already exists content: "application/json": schema: @@ -690,28 +706,28 @@ paths: parameters: - name: se-idShort1 in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string - name: se-idShortN in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string get: - summary: If the (nested) Submodel-Element is an Entity, return contained (stripped) Submodel-Elements + summary: If the (nested) SubmodelElement is an Entity, return contained (stripped) SubmodelElements operationId: ReadSubmodelSubmodelElementStatement responses: "200": - description: Returns the requested Submodel-Element + description: Success content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" "400": - description: Submodel-Element exists, but is not an Entity, so /statement is not possible. + description: SubmodelElement exists, but is not an Entity, so /statement is not possible. content: "application/json": schema: @@ -722,19 +738,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementListResult" tags: - Submodel Interface post: - summary: If the (nested) Submodel-Element is an Entity, add the Submodel-Element from the request body. + summary: If the (nested) SubmodelElement is an Entity, add a SubmodelElement to its statement operationId: CreateSubmodelSubmodelElementStatement requestBody: - description: The Submodel-Element + description: The SubmodelElement to create required: true content: "application/json": @@ -742,13 +752,13 @@ paths: $ref: "#/components/schemas/SubmodelElement" responses: "201": - description: Submodel-Element created successfully + description: Success content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" "400": - description: Submodel-Element exists, but is not an Entity, so /statement is not possible. + description: SubmodelElement exists, but is not an Entity, so /statement is not possible content: "application/json": schema: @@ -760,13 +770,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "409": - description: Submodel-Element with given idShort already exists. - content: - "application/json": - schema: - $ref: "#/components/schemas/SubmodelElementResult" - default: - description: Unexpected error + description: SubmodelElement with same idShort already exists content: "application/json": schema: @@ -777,34 +781,28 @@ paths: parameters: - name: se-idShort1 in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string - name: se-idShortN in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string get: - summary: Retrieves all Constraints from the current Submodel + summary: Returns all Constraints of the (nested) SubmodelElement operationId: ReadSubmodelSubmodelElementConstraints responses: "200": - description: Returns a list of found Constraints + description: Success content: "application/json": schema: $ref: "#/components/schemas/ConstraintListResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintListResult" - default: - description: Unexpected error + description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: @@ -812,10 +810,10 @@ paths: tags: - Submodel Interface post: - summary: Adds a new Constraint to the Submodel + summary: Adds a new Constraint to the (nested) SubmodelElement operationId: CreateSubmodelSubmodelElementConstraint requestBody: - description: The Submodel-Element + description: The Constraint to create required: true content: "application/json": @@ -823,13 +821,13 @@ paths: $ref: "#/components/schemas/Constraint" responses: "201": - description: Constraint created successfully + description: Success content: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: @@ -840,25 +838,19 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface "/{se-idShort1}/.../{se-idShortN}/constraints/{qualifier-type}": parameters: - name: se-idShort1 in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string - name: se-idShortN in: path - description: The Submodel-Element's short id + description: The SubmodelElement's idShort required: true schema: type: string @@ -869,23 +861,17 @@ paths: schema: type: string get: - summary: Retrieves a specific Qualifier from the submodel's constraints + summary: Retrieves a specific Qualifier of the (nested) SubmodelElements's Constraints (Formulas cannot be referred to yet) operationId: ReadSubmodelSubmodelElementConstraint responses: "200": - description: Returns the qualifier + description: Success content: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" "404": - description: Submodel, any SubmodelElement referred to by idShort[1-N] or qualifier not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - default: - description: Unexpected error + description: Submodel, Qualifier or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: @@ -893,30 +879,24 @@ paths: tags: - Submodel Interface put: - summary: Updates an existing qualifier in the submodel + summary: Updates an existing Qualifier in the (nested) SubmodelElement (Formulas cannot be referred to yet) operationId: UpdateSubmodelSubmodelElementConstraint requestBody: - description: The Submodel-Element + description: The Qualifier used to overwrite the existing Qualifier required: true content: "application/json": schema: - $ref: "#/components/schemas/Constraint" + $ref: "#/components/schemas/Qualifier" responses: "201": - description: Constraint created successfully + description: Success content: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] or qualifier not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - default: - description: Unexpected error + description: Submodel, Qualifier or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: @@ -924,160 +904,17 @@ paths: tags: - Submodel Interface delete: - summary: Deletes an existing qualifier from the submodel + summary: Deletes an existing Qualifier from the (nested) SubmodelElement (Formulas cannot be referred to yet) operationId: DeleteSubmodelSubmodelElementConstraint responses: "200": - description: Constraint deleted successfully - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: Any SubmodelElement referred to by idShort[1-N] or qualifier not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Submodel Interface - "/constraints": - get: - summary: Retrieves all Constraints from the current Submodel - operationId: ReadSubmodelConstraints - responses: - "200": - description: Returns a list of found Constraints - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintListResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintListResult" - tags: - - Submodel Interface - post: - summary: Adds a new Constraint to the Submodel - operationId: CreateSubmodelConstraint - requestBody: - description: The Submodel-Element - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/Constraint" - responses: - "201": - description: Constraint created successfully - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - "409": - description: "When trying to add a qualifier: Qualifier with specified type already exists" - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - tags: - - Submodel Interface - "/constraints/{qualifier-type}": - parameters: - - name: qualifier-type - in: path - description: "Type of the qualifier" - required: true - schema: - type: string - get: - summary: Retrieves a specific Qualifier from the submodel's constraints - operationId: ReadSubmodelConstraint - responses: - "200": - description: Returns the qualifier - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - "404": - description: Submodel or Constraint not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - tags: - - Submodel Interface - put: - summary: Updates an existing qualifier in the submodel - operationId: UpdateSubmodelConstraint - requestBody: - description: The Submodel-Element - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - responses: - "201": - description: Constraint created successfully - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - "404": - description: Submodel or Constraint not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - default: - description: Unexpected error - content: - "application/json": - schema: - $ref: "#/components/schemas/ConstraintResult" - tags: - - Submodel Interface - delete: - summary: Deletes an existing qualifier from the submodel - operationId: DeleteSubmodelConstraint - responses: - "200": - description: Constraint deleted successfully + description: Success content: "application/json": schema: $ref: "#/components/schemas/BaseResult" "404": - description: Submodel or Constraint not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - default: - description: Unexpected error + description: Submodel, Qualifier or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: From 7c0d55f030334af82ebf57f638e7e4525f6a19f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 6 Oct 2020 12:41:34 +0200 Subject: [PATCH 042/157] http-api-oas: fix name (uppercase P) add link to git repo of the PyI40AAS framework --- spec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec.yml b/spec.yml index 7498d9666..0e966ede5 100644 --- a/spec.yml +++ b/spec.yml @@ -1,8 +1,8 @@ openapi: 3.0.0 info: version: "1" - title: pyI40AAS REST API - description: "REST API Specification for the pyI40AAS framework. + title: PyI40AAS REST API + description: "REST API Specification for the [PyI40AAS framework](https://git.rwth-aachen.de/acplt/pyi40aas). Any identifier variables are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. From e0d184e7d71b54a47a2749180bb156b70ca02e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Oct 2020 17:35:31 +0200 Subject: [PATCH 043/157] http-api-oas: add license: EPL 2.0 --- spec.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec.yml b/spec.yml index 0e966ede5..41dedf4c2 100644 --- a/spec.yml +++ b/spec.yml @@ -14,10 +14,9 @@ info: In our implementation the AAS Interface will be available at `/aas/{identifier}` and the Submodel Interface will be available at `/submodel/{identifier}`." contact: name: "Michael Thies, Torben Miny, Leon Möller" -# TODO: which license? -# license: -# name: Use under Eclipse Public License 2.0 -# url: "https://www.eclipse.org/legal/epl-2.0/" + license: + name: Use under Eclipse Public License 2.0 + url: "https://www.eclipse.org/legal/epl-2.0/" servers: - url: http://{authority}/{basePath}/{api-version} description: This is the Server to access the Asset Administration Shell From fb1d2a6602e1eee85dd4ea5a2cb72f573dda1aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 14 Nov 2020 07:01:05 +0100 Subject: [PATCH 044/157] http-api-oas: rename Result.Error.errorType attribute: errorType -> type --- spec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec.yml b/spec.yml index 41dedf4c2..c2df4c8b6 100644 --- a/spec.yml +++ b/spec.yml @@ -933,7 +933,7 @@ components: nullable: true readOnly: true properties: - errorType: + type: enum: - Unspecified - Debug From 5796848b7dcd8f494bde277a128fb51a85e6104a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 18 Nov 2020 05:23:44 +0100 Subject: [PATCH 045/157] http-api-oas: DELETE /submodels/{submodel-identifier}: fix 404 error description --- spec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec.yml b/spec.yml index c2df4c8b6..867bb9636 100644 --- a/spec.yml +++ b/spec.yml @@ -137,7 +137,7 @@ paths: schema: $ref: "#/components/schemas/BaseResult" "404": - description: AssetAdministrationShell not found or target-identifier is not referenced + description: AssetAdministrationShell not found or the specified Submodel is not referenced content: "application/json": schema: From 94ff8a6953c0cace903e3561248b343c6c801f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 19 Nov 2020 16:28:07 +0100 Subject: [PATCH 046/157] http-api-oas: change description of 200/201 responses to "Success" --- spec.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec.yml b/spec.yml index 867bb9636..92e13ad95 100644 --- a/spec.yml +++ b/spec.yml @@ -395,7 +395,7 @@ paths: $ref: "#/components/schemas/Qualifier" responses: "201": - description: Constraint created successfully + description: Success content: "application/json": schema: @@ -413,7 +413,7 @@ paths: operationId: DeleteSubmodelConstraint responses: "200": - description: Constraint deleted successfully + description: Success content: "application/json": schema: @@ -520,7 +520,7 @@ paths: $ref: "#/components/schemas/SubmodelElement" responses: "200": - description: Returns the updated SubmodelElement + description: Success content: "application/json": schema: From 9291a04db7ab7e0062f86d24eb64527da63a2fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 19 Nov 2020 16:29:32 +0100 Subject: [PATCH 047/157] http-api-oas: make PUT routes return 200 instead of 201 --- spec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec.yml b/spec.yml index 92e13ad95..1e1515e6b 100644 --- a/spec.yml +++ b/spec.yml @@ -394,7 +394,7 @@ paths: schema: $ref: "#/components/schemas/Qualifier" responses: - "201": + "200": description: Success content: "application/json": @@ -888,7 +888,7 @@ paths: schema: $ref: "#/components/schemas/Qualifier" responses: - "201": + "200": description: Success content: "application/json": From 4b515f58ffdad92c373481aed73d15800a66d9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 19 Nov 2020 16:30:17 +0100 Subject: [PATCH 048/157] http-api-oas: add 400 responses to all PUT routes --- spec.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec.yml b/spec.yml index 1e1515e6b..5496d9d46 100644 --- a/spec.yml +++ b/spec.yml @@ -237,6 +237,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" + "400": + description: idShort of new View does not match the old one + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" "404": description: AssetAdministrationShell or View not found content: @@ -400,6 +406,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + "400": + description: type of new Qualifier does not match the old one + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" "404": description: Submodel or Constraint not found content: @@ -525,6 +537,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + "400": + description: idShort of new SubmodelElement does not match the old one + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" "404": description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: @@ -894,6 +912,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + "400": + description: type of new Qualifier does not match the old one + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" "404": description: Submodel, Qualifier or any SubmodelElement referred to by idShort[1-N] not found content: From a4b32c0d4331487263a71b7b39b4d28caa87aee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 19 Nov 2020 16:30:55 +0100 Subject: [PATCH 049/157] http-api-oas: 201 responses from POST routes should have a Location header --- spec.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/spec.yml b/spec.yml index 5496d9d46..c27572340 100644 --- a/spec.yml +++ b/spec.yml @@ -86,6 +86,11 @@ paths: "application/json": schema: $ref: "#/components/schemas/ReferenceResult" + headers: + Location: + description: The URL of the created Submodel-Reference + schema: + type: string "404": description: AssetAdministrationShell not found content: @@ -180,6 +185,11 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The URL of the created View + schema: + type: string "404": description: AssetAdministrationShell not found content: @@ -349,6 +359,11 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + headers: + Location: + description: The URL of the created Constraint + schema: + type: string "404": description: Submodel not found content: @@ -474,6 +489,11 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The URL of the created SubmodelElement + schema: + type: string "404": description: Submodel not found content: @@ -624,6 +644,11 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The URL of the created SubmodelElement + schema: + type: string "400": description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: @@ -699,6 +724,11 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The URL of the created SubmodelElement + schema: + type: string "400": description: SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible content: @@ -774,6 +804,11 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The URL of the created SubmodelElement + schema: + type: string "400": description: SubmodelElement exists, but is not an Entity, so /statement is not possible content: @@ -843,6 +878,11 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + headers: + Location: + description: The URL of the created Constraint + schema: + type: string "404": description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: From 170c731dc614363562651a5790f45a1f7cba19e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 23 Nov 2020 17:29:45 +0100 Subject: [PATCH 050/157] http-api-oas: updating a nested SubmodelElement with a different type is a BadRequest (400) --- spec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec.yml b/spec.yml index c27572340..307eadbf6 100644 --- a/spec.yml +++ b/spec.yml @@ -558,7 +558,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "400": - description: idShort of new SubmodelElement does not match the old one + description: type or idShort of new SubmodelElement does not match the old one content: "application/json": schema: From 038fa111a0a9248d5ff6e3e0d2441d874b69c249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 16 Dec 2020 03:37:16 +0100 Subject: [PATCH 051/157] http-api-oas: idShorts are prefixed with an exclammation mark in the nested route --- spec.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/spec.yml b/spec.yml index 307eadbf6..17e92c899 100644 --- a/spec.yml +++ b/spec.yml @@ -512,13 +512,13 @@ paths: parameters: - name: se-idShort1 in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string - name: se-idShortN in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string @@ -593,13 +593,13 @@ paths: parameters: - name: se-idShort1 in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string - name: se-idShortN in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string @@ -673,13 +673,13 @@ paths: parameters: - name: se-idShort1 in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string - name: se-idShortN in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string @@ -753,13 +753,13 @@ paths: parameters: - name: se-idShort1 in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string - name: se-idShortN in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string @@ -833,13 +833,13 @@ paths: parameters: - name: se-idShort1 in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string - name: se-idShortN in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string @@ -901,13 +901,13 @@ paths: parameters: - name: se-idShort1 in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string - name: se-idShortN in: path - description: The SubmodelElement's idShort + description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) required: true schema: type: string From f16e99996d70146749338d0e5ed6693416a68ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 22 Jan 2021 16:40:15 +0100 Subject: [PATCH 052/157] http-api-oas: update json schema to V3.0RC01 --- spec.yml | 508 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 293 insertions(+), 215 deletions(-) diff --git a/spec.yml b/spec.yml index 17e92c899..66c51f17b 100644 --- a/spec.yml +++ b/spec.yml @@ -1083,32 +1083,56 @@ components: type: array items: $ref: "#/components/schemas/Constraint" + StrippedAssetAdministrationShell: + allOf: + - $ref: "#/components/schemas/AssetAdministrationShell" + - properties: + views: + not: {} + submodels: + not: {} + conceptDictionaries: + not: {} + StrippedSubmodel: + allOf: + - $ref: "#/components/schemas/Submodel" + - properties: + submodelElements: + not: {} + qualifiers: + not: {} + StrippedSubmodelElement: + allOf: + - $ref: "#/components/schemas/SubmodelElement" + - properties: + qualifiers: + not: {} Referable: - type: object - properties: - idShort: - type: string - category: - type: string - description: - type: array - items: - $ref: "#/components/schemas/LangString" - parent: - $ref: "#/components/schemas/Reference" - modelType: - $ref: "#/components/schemas/ModelType" - required: - - idShort - - modelType + allOf: + - $ref: '#/components/schemas/HasExtensions' + - properties: + idShort: + type: string + category: + type: string + displayName: + type: string + description: + type: array + items: + $ref: '#/components/schemas/LangString' + modelType: + $ref: '#/components/schemas/ModelType' + required: + - modelType Identifiable: allOf: - - $ref: "#/components/schemas/Referable" + - $ref: '#/components/schemas/Referable' - properties: identification: - $ref: "#/components/schemas/Identifier" + $ref: '#/components/schemas/Identifier' administration: - $ref: "#/components/schemas/AdministrativeInformation" + $ref: '#/components/schemas/AdministrativeInformation' required: - identification Qualifiable: @@ -1117,61 +1141,113 @@ components: qualifiers: type: array items: - $ref: "#/components/schemas/Constraint" + $ref: '#/components/schemas/Constraint' HasSemantics: type: object properties: semanticId: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' HasDataSpecification: type: object properties: embeddedDataSpecifications: type: array items: - $ref: "#/components/schemas/EmbeddedDataSpecification" + $ref: '#/components/schemas/EmbeddedDataSpecification' + HasExtensions: + type: object + properties: + extensions: + type: array + items: + $ref: '#/components/schemas/Extension' + Extension: + allOf: + - $ref: '#/components/schemas/HasSemantics' + - properties: + name: + type: string + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + value: + type: string + refersTo: + $ref: '#/components/schemas/Reference' + required: + - name AssetAdministrationShell: allOf: - - $ref: "#/components/schemas/Identifiable" - - $ref: "#/components/schemas/HasDataSpecification" + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' - properties: derivedFrom: - $ref: "#/components/schemas/Reference" - asset: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' + assetInformation: + $ref: '#/components/schemas/AssetInformation' submodels: type: array items: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' views: type: array items: - $ref: "#/components/schemas/View" - conceptDictionaries: - type: array - items: - $ref: "#/components/schemas/ConceptDictionary" + $ref: '#/components/schemas/View' security: - $ref: "#/components/schemas/Security" + $ref: '#/components/schemas/Security' required: - - asset - StrippedAssetAdministrationShell: - allOf: - - $ref: "#/components/schemas/AssetAdministrationShell" - - properties: - views: - not: {} - submodels: - not: {} - conceptDictionaries: - not: {} + - assetInformation Identifier: type: object properties: id: type: string idType: - $ref: "#/components/schemas/KeyType" + $ref: '#/components/schemas/KeyType' required: - id - idType @@ -1206,25 +1282,22 @@ components: keys: type: array items: - $ref: "#/components/schemas/Key" + $ref: '#/components/schemas/Key' required: - keys Key: type: object properties: type: - $ref: "#/components/schemas/KeyElements" + $ref: '#/components/schemas/KeyElements' idType: - $ref: "#/components/schemas/KeyType" + $ref: '#/components/schemas/KeyType' value: type: string - local: - type: boolean required: - type - idType - value - - local KeyElements: type: string enum: @@ -1237,7 +1310,6 @@ components: - BasicEvent - Blob - Capability - - ConceptDictionary - DataElement - File - Entity @@ -1265,7 +1337,6 @@ components: - BasicEvent - Blob - Capability - - ConceptDictionary - DataElement - File - Entity @@ -1288,23 +1359,23 @@ components: type: object properties: name: - $ref: "#/components/schemas/ModelTypes" + $ref: '#/components/schemas/ModelTypes' required: - name EmbeddedDataSpecification: type: object properties: dataSpecification: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' dataSpecificationContent: - $ref: "#/components/schemas/DataSpecificationContent" + $ref: '#/components/schemas/DataSpecificationContent' required: - dataSpecification - dataSpecificationContent DataSpecificationContent: oneOf: - - $ref: "#/components/schemas/DataSpecificationIEC61360Content" - - $ref: "#/components/schemas/DataSpecificationPhysicalUnitContent" + - $ref: '#/components/schemas/DataSpecificationIEC61360Content' + - $ref: '#/components/schemas/DataSpecificationPhysicalUnitContent' DataSpecificationPhysicalUnitContent: type: object properties: @@ -1315,7 +1386,7 @@ components: definition: type: array items: - $ref: "#/components/schemas/LangString" + $ref: '#/components/schemas/LangString' siNotation: type: string siName: @@ -1342,7 +1413,7 @@ components: - definition DataSpecificationIEC61360Content: allOf: - - $ref: "#/components/schemas/ValueObject" + - $ref: '#/components/schemas/ValueObject' - type: object properties: dataType: @@ -1365,15 +1436,15 @@ components: definition: type: array items: - $ref: "#/components/schemas/LangString" + $ref: '#/components/schemas/LangString' preferredName: type: array items: - $ref: "#/components/schemas/LangString" + $ref: '#/components/schemas/LangString' shortName: type: array items: - $ref: "#/components/schemas/LangString" + $ref: '#/components/schemas/LangString' sourceOfDefinition: type: string symbol: @@ -1381,15 +1452,15 @@ components: unit: type: string unitId: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' valueFormat: type: string valueList: - $ref: "#/components/schemas/ValueList" + $ref: '#/components/schemas/ValueList' levelType: type: array items: - $ref: "#/components/schemas/LevelType" + $ref: '#/components/schemas/LevelType' required: - preferredName LevelType: @@ -1406,19 +1477,19 @@ components: type: array minItems: 1 items: - $ref: "#/components/schemas/ValueReferencePairType" + $ref: '#/components/schemas/ValueReferencePairType' required: - valueReferencePairTypes ValueReferencePairType: allOf: - - $ref: "#/components/schemas/ValueObject" + - $ref: '#/components/schemas/ValueObject' ValueObject: type: object properties: value: type: string valueId: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' valueType: type: string enum: @@ -1468,17 +1539,41 @@ components: - time Asset: allOf: - - $ref: "#/components/schemas/Identifiable" - - $ref: "#/components/schemas/HasDataSpecification" + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + AssetInformation: + allOf: - properties: - kind: - $ref: "#/components/schemas/AssetKind" - assetIdentificationModel: - $ref: "#/components/schemas/Reference" + assetKind: + $ref: '#/components/schemas/AssetKind' + globalAssetId: + $ref: '#/components/schemas/Reference' + externalAssetIds: + type: array + items: + $ref: '#/components/schemas/IdentifierKeyValuePair' billOfMaterial: - $ref: "#/components/schemas/Reference" + type: array + items: + $ref: '#/components/schemas/Reference' + thumbnail: + $ref: '#/components/schemas/File' + required: + - assetKind + IdentifierKeyValuePair: + allOf: + - $ref: '#/components/schemas/HasSemantics' + - properties: + key: + type: string + value: + type: string + subjectId: + $ref: '#/components/schemas/Reference' required: - - kind + - key + - value + - subjectId AssetKind: type: string enum: @@ -1491,92 +1586,82 @@ components: - Instance Submodel: allOf: - - $ref: "#/components/schemas/Identifiable" - - $ref: "#/components/schemas/HasDataSpecification" - - $ref: "#/components/schemas/Qualifiable" - - $ref: "#/components/schemas/HasSemantics" + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/Qualifiable' + - $ref: '#/components/schemas/HasSemantics' - properties: kind: - $ref: "#/components/schemas/ModelingKind" + $ref: '#/components/schemas/ModelingKind' submodelElements: type: array items: - $ref: "#/components/schemas/SubmodelElement" - StrippedSubmodel: - allOf: - - $ref: "#/components/schemas/Submodel" - - properties: - submodelElements: - not: {} - qualifiers: - not: {} + $ref: '#/components/schemas/SubmodelElement' Constraint: type: object properties: modelType: - $ref: "#/components/schemas/ModelType" + $ref: '#/components/schemas/ModelType' required: - modelType Operation: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' - properties: inputVariable: type: array items: - $ref: "#/components/schemas/OperationVariable" + $ref: '#/components/schemas/OperationVariable' outputVariable: type: array items: - $ref: "#/components/schemas/OperationVariable" + $ref: '#/components/schemas/OperationVariable' inoutputVariable: type: array items: - $ref: "#/components/schemas/OperationVariable" + $ref: '#/components/schemas/OperationVariable' OperationVariable: type: object properties: value: oneOf: - - $ref: "#/components/schemas/Blob" - - $ref: "#/components/schemas/File" - - $ref: "#/components/schemas/Capability" - - $ref: "#/components/schemas/Entity" - - $ref: "#/components/schemas/Event" - - $ref: "#/components/schemas/BasicEvent" - - $ref: "#/components/schemas/MultiLanguageProperty" - - $ref: "#/components/schemas/Operation" - - $ref: "#/components/schemas/Property" - - $ref: "#/components/schemas/Range" - - $ref: "#/components/schemas/ReferenceElement" - - $ref: "#/components/schemas/RelationshipElement" - - $ref: "#/components/schemas/SubmodelElementCollection" + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/Capability' + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Event' + - $ref: '#/components/schemas/BasicEvent' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Operation' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + - $ref: '#/components/schemas/RelationshipElement' + - $ref: '#/components/schemas/SubmodelElementCollection' required: - value SubmodelElement: allOf: - - $ref: "#/components/schemas/Referable" - - $ref: "#/components/schemas/HasDataSpecification" - - $ref: "#/components/schemas/HasSemantics" - - $ref: "#/components/schemas/Qualifiable" + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/HasSemantics' + - $ref: '#/components/schemas/Qualifiable' - properties: kind: - $ref: "#/components/schemas/ModelingKind" - StrippedSubmodelElement: - allOf: - - $ref: "#/components/schemas/SubmodelElement" - - properties: - qualifiers: - not: {} + $ref: '#/components/schemas/ModelingKind' + idShort: + type: string + required: + - idShort Event: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' BasicEvent: allOf: - - $ref: "#/components/schemas/Event" + - $ref: '#/components/schemas/Event' - properties: observed: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' required: - observed EntityType: @@ -1586,56 +1671,49 @@ components: - SelfManagedEntity Entity: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' - properties: statements: type: array items: - $ref: "#/components/schemas/SubmodelElement" + $ref: '#/components/schemas/SubmodelElement' entityType: - $ref: "#/components/schemas/EntityType" - asset: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/EntityType' + globalAssetId: + $ref: '#/components/schemas/Reference' + specificAssetIds: + $ref: '#/components/schemas/IdentifierKeyValuePair' required: - entityType View: allOf: - - $ref: "#/components/schemas/Referable" - - $ref: "#/components/schemas/HasDataSpecification" - - $ref: "#/components/schemas/HasSemantics" + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/HasSemantics' - properties: containedElements: type: array items: - $ref: "#/components/schemas/Reference" - ConceptDictionary: - allOf: - - $ref: "#/components/schemas/Referable" - - $ref: "#/components/schemas/HasDataSpecification" - - properties: - conceptDescriptions: - type: array - items: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' ConceptDescription: allOf: - - $ref: "#/components/schemas/Identifiable" - - $ref: "#/components/schemas/HasDataSpecification" + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' - properties: isCaseOf: type: array items: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' Capability: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' Property: allOf: - - $ref: "#/components/schemas/SubmodelElement" - - $ref: "#/components/schemas/ValueObject" + - $ref: '#/components/schemas/SubmodelElement' + - $ref: '#/components/schemas/ValueObject' Range: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' - properties: valueType: type: string @@ -1692,17 +1770,17 @@ components: - valueType MultiLanguageProperty: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' - properties: value: type: array items: - $ref: "#/components/schemas/LangString" + $ref: '#/components/schemas/LangString' valueId: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' File: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' - properties: value: type: string @@ -1712,7 +1790,7 @@ components: - mimeType Blob: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' - properties: value: type: string @@ -1722,65 +1800,65 @@ components: - mimeType ReferenceElement: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' - properties: value: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' SubmodelElementCollection: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' - properties: value: type: array items: oneOf: - - $ref: "#/components/schemas/Blob" - - $ref: "#/components/schemas/File" - - $ref: "#/components/schemas/Capability" - - $ref: "#/components/schemas/Entity" - - $ref: "#/components/schemas/Event" - - $ref: "#/components/schemas/BasicEvent" - - $ref: "#/components/schemas/MultiLanguageProperty" - - $ref: "#/components/schemas/Operation" - - $ref: "#/components/schemas/Property" - - $ref: "#/components/schemas/Range" - - $ref: "#/components/schemas/ReferenceElement" - - $ref: "#/components/schemas/RelationshipElement" - - $ref: "#/components/schemas/SubmodelElementCollection" + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/Capability' + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Event' + - $ref: '#/components/schemas/BasicEvent' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Operation' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + - $ref: '#/components/schemas/RelationshipElement' + - $ref: '#/components/schemas/SubmodelElementCollection' allowDuplicates: type: boolean ordered: type: boolean RelationshipElement: allOf: - - $ref: "#/components/schemas/SubmodelElement" + - $ref: '#/components/schemas/SubmodelElement' - properties: first: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' second: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' required: - first - second AnnotatedRelationshipElement: allOf: - - $ref: "#/components/schemas/RelationshipElement" + - $ref: '#/components/schemas/RelationshipElement' - properties: annotation: type: array items: oneOf: - - $ref: "#/components/schemas/Blob" - - $ref: "#/components/schemas/File" - - $ref: "#/components/schemas/MultiLanguageProperty" - - $ref: "#/components/schemas/Property" - - $ref: "#/components/schemas/Range" - - $ref: "#/components/schemas/ReferenceElement" + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' Qualifier: allOf: - - $ref: "#/components/schemas/Constraint" - - $ref: "#/components/schemas/HasSemantics" - - $ref: "#/components/schemas/ValueObject" + - $ref: '#/components/schemas/Constraint' + - $ref: '#/components/schemas/HasSemantics' + - $ref: '#/components/schemas/ValueObject' - properties: type: type: string @@ -1788,53 +1866,53 @@ components: - type Formula: allOf: - - $ref: "#/components/schemas/Constraint" + - $ref: '#/components/schemas/Constraint' - properties: dependsOn: type: array items: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' Security: type: object properties: accessControlPolicyPoints: - $ref: "#/components/schemas/AccessControlPolicyPoints" + $ref: '#/components/schemas/AccessControlPolicyPoints' certificate: type: array items: oneOf: - - $ref: "#/components/schemas/BlobCertificate" + - $ref: '#/components/schemas/BlobCertificate' requiredCertificateExtension: type: array items: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' required: - accessControlPolicyPoints Certificate: type: object BlobCertificate: allOf: - - $ref: "#/components/schemas/Certificate" + - $ref: '#/components/schemas/Certificate' - properties: blobCertificate: - $ref: "#/components/schemas/Blob" + $ref: '#/components/schemas/Blob' containedExtension: type: array items: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' lastCertificate: type: boolean AccessControlPolicyPoints: type: object properties: policyAdministrationPoint: - $ref: "#/components/schemas/PolicyAdministrationPoint" + $ref: '#/components/schemas/PolicyAdministrationPoint' policyDecisionPoint: - $ref: "#/components/schemas/PolicyDecisionPoint" + $ref: '#/components/schemas/PolicyDecisionPoint' policyEnforcementPoint: - $ref: "#/components/schemas/PolicyEnforcementPoint" + $ref: '#/components/schemas/PolicyEnforcementPoint' policyInformationPoints: - $ref: "#/components/schemas/PolicyInformationPoints" + $ref: '#/components/schemas/PolicyInformationPoints' required: - policyAdministrationPoint - policyDecisionPoint @@ -1843,7 +1921,7 @@ components: type: object properties: localAccessControl: - $ref: "#/components/schemas/AccessControl" + $ref: '#/components/schemas/AccessControl' externalAccessControl: type: boolean required: @@ -1854,7 +1932,7 @@ components: internalInformationPoint: type: array items: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' externalInformationPoint: type: boolean required: @@ -1877,35 +1955,35 @@ components: type: object properties: selectableSubjectAttributes: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' defaultSubjectAttributes: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' selectablePermissions: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' defaultPermissions: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' selectableEnvironmentAttributes: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' defaultEnvironmentAttributes: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' accessPermissionRule: type: array items: - $ref: "#/components/schemas/AccessPermissionRule" + $ref: '#/components/schemas/AccessPermissionRule' AccessPermissionRule: allOf: - - $ref: "#/components/schemas/Referable" - - $ref: "#/components/schemas/Qualifiable" + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/Qualifiable' - properties: targetSubjectAttributes: type: array items: - $ref: "#/components/schemas/SubjectAttributes" + $ref: '#/components/schemas/SubjectAttributes' minItems: 1 permissionsPerObject: type: array items: - $ref: "#/components/schemas/PermissionsPerObject" + $ref: '#/components/schemas/PermissionsPerObject' required: - targetSubjectAttributes SubjectAttributes: @@ -1914,32 +1992,32 @@ components: subjectAttributes: type: array items: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' minItems: 1 PermissionsPerObject: type: object properties: object: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' targetObjectAttributes: - $ref: "#/components/schemas/ObjectAttributes" + $ref: '#/components/schemas/ObjectAttributes' permission: type: array items: - $ref: "#/components/schemas/Permission" + $ref: '#/components/schemas/Permission' ObjectAttributes: type: object properties: objectAttribute: type: array items: - $ref: "#/components/schemas/Property" + $ref: '#/components/schemas/Property' minItems: 1 Permission: type: object properties: permission: - $ref: "#/components/schemas/Reference" + $ref: '#/components/schemas/Reference' kindOfPermission: type: string enum: From 79d812e158a20b66454d9998b3c421a3519e4a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 22 Jan 2021 16:41:44 +0100 Subject: [PATCH 053/157] http-api-oas: update description --- spec.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/spec.yml b/spec.yml index 66c51f17b..df89f7dfe 100644 --- a/spec.yml +++ b/spec.yml @@ -4,14 +4,22 @@ info: title: PyI40AAS REST API description: "REST API Specification for the [PyI40AAS framework](https://git.rwth-aachen.de/acplt/pyi40aas). - - Any identifier variables are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. + The git repository of this document is available [here](https://git.rwth-aachen.de/leon.moeller/pyi40aas-oas). + + + --- + + **General:** + + + Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. + This specification contains one exemplary `PATCH` route. In the future there should be a `PATCH` route for every `PUT` route, where it makes sense. - - In our implementation the AAS Interface will be available at `/aas/{identifier}` and the Submodel Interface will be available at `/submodel/{identifier}`." + + In our implementation the AAS Interface is available at `/aas/{identifier}` and the Submodel Interface at `/submodel/{identifier}`." contact: name: "Michael Thies, Torben Miny, Leon Möller" license: From 1658e75532dbab534b89c3a80cd2fc77ff1441e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 3 Mar 2021 04:32:50 +0100 Subject: [PATCH 054/157] http-api-oas: fix description of some 404 responses --- spec.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec.yml b/spec.yml index df89f7dfe..f9e9eebcf 100644 --- a/spec.yml +++ b/spec.yml @@ -708,7 +708,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred to by idShort[1-N] not found content: "application/json": schema: @@ -744,7 +744,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort[1-N] not found content: "application/json": schema: @@ -788,7 +788,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort[1-N] not found content: "application/json": schema: @@ -824,7 +824,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "404": - description: Any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort[1-N] not found content: "application/json": schema: From 35316a33d1cd43d3df339081a8a4b739484b463d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 3 Mar 2021 04:58:45 +0100 Subject: [PATCH 055/157] http-api-oas: change {se-idShort1}/.../{se-idShortN} to {idShort-path} --- spec.yml | 100 ++++++++++++++++++------------------------------------- 1 file changed, 32 insertions(+), 68 deletions(-) diff --git a/spec.yml b/spec.yml index f9e9eebcf..e81c0e8a6 100644 --- a/spec.yml +++ b/spec.yml @@ -516,17 +516,11 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{se-idShort1}/.../{se-idShortN}": + "/{idShort-path}": parameters: - - name: se-idShort1 + - name: idShort-path in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) - required: true - schema: - type: string - - name: se-idShortN - in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) + description: A /-separated concatenation of !-prefixed idShorts required: true schema: type: string @@ -541,7 +535,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: Submodel or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -572,7 +566,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "404": - description: Submodel or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -590,24 +584,18 @@ paths: schema: $ref: "#/components/schemas/BaseResult" "404": - description: Submodel or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface - "/{se-idShort1}/.../{se-idShortN}/value": + "/{idShort-path}/value": parameters: - - name: se-idShort1 - in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) - required: true - schema: - type: string - - name: se-idShortN + - name: idShort-path in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) + description: A /-separated concatenation of !-prefixed idShorts required: true schema: type: string @@ -628,7 +616,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: Submodel or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -664,7 +652,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "404": - description: Submodel or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -677,17 +665,11 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{se-idShort1}/.../{se-idShortN}/annotation": + "/{idShort-path}/annotation": parameters: - - name: se-idShort1 - in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) - required: true - schema: - type: string - - name: se-idShortN + - name: idShort-path in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) + description: A /-separated concatenation of !-prefixed idShorts required: true schema: type: string @@ -708,7 +690,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: Submodel or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -744,7 +726,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "404": - description: Submodel or any SubmodelElement referred by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -757,17 +739,11 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{se-idShort1}/.../{se-idShortN}/statement": + "/{idShort-path}/statement": parameters: - - name: se-idShort1 + - name: idShort-path in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) - required: true - schema: - type: string - - name: se-idShortN - in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) + description: A /-separated concatenation of !-prefixed idShorts required: true schema: type: string @@ -788,7 +764,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "404": - description: Submodel or any SubmodelElement referred by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -824,7 +800,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementResult" "404": - description: Submodel or any SubmodelElement referred by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -837,17 +813,11 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{se-idShort1}/.../{se-idShortN}/constraints": + "/{idShort-path}/constraints": parameters: - - name: se-idShort1 - in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) - required: true - schema: - type: string - - name: se-idShortN + - name: idShort-path in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) + description: A /-separated concatenation of !-prefixed idShorts required: true schema: type: string @@ -862,7 +832,7 @@ paths: schema: $ref: "#/components/schemas/ConstraintListResult" "404": - description: Submodel or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -892,7 +862,7 @@ paths: schema: type: string "404": - description: Submodel or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -905,17 +875,11 @@ paths: $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface - "/{se-idShort1}/.../{se-idShortN}/constraints/{qualifier-type}": + "/{idShort-path}/constraints/{qualifier-type}": parameters: - - name: se-idShort1 - in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) - required: true - schema: - type: string - - name: se-idShortN + - name: idShort-path in: path - description: The SubmodelElement's idShort, prefixed with an exclammation mark (!) + description: A /-separated concatenation of !-prefixed idShorts required: true schema: type: string @@ -936,7 +900,7 @@ paths: schema: $ref: "#/components/schemas/ConstraintResult" "404": - description: Submodel, Qualifier or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -967,7 +931,7 @@ paths: schema: $ref: "#/components/schemas/ConstraintResult" "404": - description: Submodel, Qualifier or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: "application/json": schema: @@ -985,7 +949,7 @@ paths: schema: $ref: "#/components/schemas/BaseResult" "404": - description: Submodel, Qualifier or any SubmodelElement referred to by idShort[1-N] not found + description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: "application/json": schema: From 383114f1551a2ac29d48b1335307c06fbb3bdf77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 3 Mar 2021 05:08:58 +0100 Subject: [PATCH 056/157] http-api-oas: add 422 responses to POST routes --- spec.yml | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/spec.yml b/spec.yml index e81c0e8a6..3d625ba7a 100644 --- a/spec.yml +++ b/spec.yml @@ -111,6 +111,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ReferenceResult" + "422": + description: Request body is not a Reference or not resolvable + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" tags: - Asset Administration Shell Interface "/submodels/{submodel-identifier}": @@ -210,6 +216,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface "/views/{view-idShort}": @@ -384,6 +396,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + "422": + description: Request body is not a valid Qualifier + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface "/constraints/{qualifier-type}": @@ -514,6 +532,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface "/{idShort-path}": @@ -663,6 +687,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface "/{idShort-path}/annotation": @@ -737,6 +767,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface "/{idShort-path}/statement": @@ -811,6 +847,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface "/{idShort-path}/constraints": @@ -873,6 +915,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + "422": + description: Request body is not a valid Qualifier + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface "/{idShort-path}/constraints/{qualifier-type}": From c27232afdcc82ea123280e82fc38187cfa42e233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 3 Mar 2021 05:22:16 +0100 Subject: [PATCH 057/157] http-api-oas: allow changing the identifying attribute (idShort or type) in PUT/PATCH routes --- spec.yml | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 8 deletions(-) diff --git a/spec.yml b/spec.yml index 3d625ba7a..13c4199b7 100644 --- a/spec.yml +++ b/spec.yml @@ -267,18 +267,35 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" - "400": - description: idShort of new View does not match the old one + "201": + description: Success (idShort changed) content: "application/json": schema: $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The new URL of the View + schema: + type: string "404": description: AssetAdministrationShell or View not found content: "application/json": schema: $ref: "#/components/schemas/ViewResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface patch: @@ -298,12 +315,35 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" + "201": + description: Success (idShort changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The new URL of the View + schema: + type: string "404": description: AssetAdministrationShell or View not found content: "application/json": schema: $ref: "#/components/schemas/ViewResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface delete: @@ -447,18 +487,35 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" - "400": - description: type of new Qualifier does not match the old one + "201": + description: Success (type changed) content: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + headers: + Location: + description: The new URL of the Qualifier + schema: + type: string "404": description: Submodel or Constraint not found content: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + "409": + description: type changed and new type already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "422": + description: Request body is not a valid Qualifier + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface delete: @@ -583,18 +640,35 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" - "400": - description: type or idShort of new SubmodelElement does not match the old one + "201": + description: Success (idShort changed) content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + headers: + Location: + description: The new URL of the SubmodelElement + schema: + type: string "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: $ref: "#/components/schemas/SubmodelElementResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" + "422": + description: Request body is not a valid SubmodelElement **or** the type of the new SubmodelElement differs from the existing one + content: + "application/json": + schema: + $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface delete: @@ -972,18 +1046,35 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" - "400": - description: type of new Qualifier does not match the old one + "201": + description: Success (type changed) content: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + headers: + Location: + description: The new URL of the Qualifier + schema: + type: string "404": description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + "409": + description: type changed and new type already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" + "422": + description: Request body is not a valid Qualifier + content: + "application/json": + schema: + $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface delete: From b7573db5ab1377263555ebb98de09864faf4abb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 18:33:38 +0200 Subject: [PATCH 058/157] http-api-oas: move to test/adapter/ directory improve filename use .yaml instead of .yml file extension (see https://yaml.org/faq.html) --- spec.yml => test/adapter/http-api-oas.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec.yml => test/adapter/http-api-oas.yaml (100%) diff --git a/spec.yml b/test/adapter/http-api-oas.yaml similarity index 100% rename from spec.yml rename to test/adapter/http-api-oas.yaml From b0a2984d93455353eaef8f8f18ab73ed44648762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 22:17:05 +0200 Subject: [PATCH 059/157] http-api-oas: split into aas and submodel interface --- test/adapter/http-api-oas-aas.yaml | 1399 +++++++++++++++++ ...pi-oas.yaml => http-api-oas-submodel.yaml} | 340 +--- 2 files changed, 1401 insertions(+), 338 deletions(-) create mode 100644 test/adapter/http-api-oas-aas.yaml rename test/adapter/{http-api-oas.yaml => http-api-oas-submodel.yaml} (83%) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml new file mode 100644 index 000000000..2d6b06276 --- /dev/null +++ b/test/adapter/http-api-oas-aas.yaml @@ -0,0 +1,1399 @@ +openapi: 3.0.0 +info: + version: "1" + title: PyI40AAS REST API + description: "REST API Specification for the [PyI40AAS framework](https://git.rwth-aachen.de/acplt/pyi40aas). + + + **AAS Interface** + + + Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`." + contact: + name: "Michael Thies, Torben Miny, Leon Möller" + license: + name: Use under Eclipse Public License 2.0 + url: "https://www.eclipse.org/legal/epl-2.0/" +servers: + - url: http://{authority}/{basePath}/{api-version} + description: This is the Server to access the Asset Administration Shell + variables: + authority: + default: localhost:8080 + description: The authority is the server url (made of IP-Address or DNS-Name, user information, and/or port information) of the hosting environment for the Asset Administration Shell + basePath: + default: api + description: The basePath variable is additional path information for the hosting environment. It may contain the name of an aggregation point like 'shells' and/or API version information and/or tenant-id information, etc. + api-version: + default: v1 + description: The Version of the API-Specification +paths: + "/": + get: + summary: Retrieves the stripped AssetAdministrationShell, without Submodel-References and Views. + operationId: ReadAAS + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/AASResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/AASResult" + tags: + - Asset Administration Shell Interface + "/submodels": + get: + summary: Returns all Submodel-References of the AssetAdministrationShell + operationId: ReadAASSubmodelReferences + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceListResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceListResult" + tags: + - Asset Administration Shell Interface + post: + summary: Adds a new Submodel-Reference to the AssetAdministrationShell + operationId: CreateAASSubmodelReference + requestBody: + description: The Submodel-Reference to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/Reference" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + headers: + Location: + description: The URL of the created Submodel-Reference + schema: + type: string + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + "409": + description: Submodel-Reference already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + "422": + description: Request body is not a Reference or not resolvable + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + tags: + - Asset Administration Shell Interface + "/submodels/{submodel-identifier}": + parameters: + - name: submodel-identifier + in: path + description: The Identifier of the referenced Submodel + required: true + schema: + type: string + get: + summary: Returns the Reference specified by submodel-identifier + operationId: ReadAASSubmodelReference + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + "404": + description: AssetAdministrationShell not found or the specified Submodel is not referenced + content: + "application/json": + schema: + $ref: "#/components/schemas/ReferenceResult" + tags: + - Asset Administration Shell Interface + delete: + summary: Deletes the Reference specified by submodel-identifier + operationId: DeleteAASSubmodelReference + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: AssetAdministrationShell not found or the specified Submodel is not referenced + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Asset Administration Shell Interface + "/views": + get: + summary: Returns all Views of the AssetAdministrationShell + operationId: ReadAASViews + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewListResult" + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewListResult" + tags: + - Asset Administration Shell Interface + post: + summary: Adds a new View to the AssetAdministrationShell + operationId: CreateAASView + requestBody: + description: The View to create + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" + responses: + "201": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The URL of the created View + schema: + type: string + "404": + description: AssetAdministrationShell not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "409": + description: View with same idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + "/views/{view-idShort}": + parameters: + - name: view-idShort + in: path + description: The idShort of the View + required: true + schema: + type: string + get: + summary: Returns a specific View of the AssetAdministrationShell + operationId: ReadAASView + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + put: + summary: Updates a specific View of the AssetAdministrationShell + operationId: UpdateAASView + requestBody: + description: The View used to overwrite the existing View + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "201": + description: Success (idShort changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The new URL of the View + schema: + type: string + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + patch: + summary: Updates a specific View of the AssetAdministrationShell by only providing properties that should be changed + operationId: PatchAASView + requestBody: + description: The (partial) View used to update the existing View + required: true + content: + "application/json": + schema: + $ref: "#/components/schemas/View" + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "201": + description: Success (idShort changed) + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + headers: + Location: + description: The new URL of the View + schema: + type: string + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "409": + description: idShort changed and new idShort already exists + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + "422": + description: Request body is not a valid View + content: + "application/json": + schema: + $ref: "#/components/schemas/ViewResult" + tags: + - Asset Administration Shell Interface + delete: + summary: Deletes a specific View from the Asset Administration Shell + operationId: DeleteAASView + responses: + "200": + description: Success + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + "404": + description: AssetAdministrationShell or View not found + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" + tags: + - Asset Administration Shell Interface +components: + schemas: + BaseResult: + type: object + properties: + success: + type: boolean + readOnly: true + error: + type: object + nullable: true + readOnly: true + properties: + type: + enum: + - Unspecified + - Debug + - Information + - Warning + - Error + - Fatal + - Exception + type: string + code: + type: string + text: + type: string + data: + nullable: true + readOnly: true + type: object + AASResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedAssetAdministrationShell" + ReferenceResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/Reference" + ReferenceListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/Reference" + ViewResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/View" + ViewListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/View" + SubmodelResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedSubmodel" + SubmodelElementResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/StrippedSubmodelElement" + SubmodelElementListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/StrippedSubmodelElement" + ConstraintResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + $ref: "#/components/schemas/Constraint" + ConstraintListResult: + allOf: + - $ref: "#/components/schemas/BaseResult" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/Constraint" + StrippedAssetAdministrationShell: + allOf: + - $ref: "#/components/schemas/AssetAdministrationShell" + - properties: + views: + not: {} + submodels: + not: {} + conceptDictionaries: + not: {} + StrippedSubmodel: + allOf: + - $ref: "#/components/schemas/Submodel" + - properties: + submodelElements: + not: {} + qualifiers: + not: {} + StrippedSubmodelElement: + allOf: + - $ref: "#/components/schemas/SubmodelElement" + - properties: + qualifiers: + not: {} + Referable: + allOf: + - $ref: '#/components/schemas/HasExtensions' + - properties: + idShort: + type: string + category: + type: string + displayName: + type: string + description: + type: array + items: + $ref: '#/components/schemas/LangString' + modelType: + $ref: '#/components/schemas/ModelType' + required: + - modelType + Identifiable: + allOf: + - $ref: '#/components/schemas/Referable' + - properties: + identification: + $ref: '#/components/schemas/Identifier' + administration: + $ref: '#/components/schemas/AdministrativeInformation' + required: + - identification + Qualifiable: + type: object + properties: + qualifiers: + type: array + items: + $ref: '#/components/schemas/Constraint' + HasSemantics: + type: object + properties: + semanticId: + $ref: '#/components/schemas/Reference' + HasDataSpecification: + type: object + properties: + embeddedDataSpecifications: + type: array + items: + $ref: '#/components/schemas/EmbeddedDataSpecification' + HasExtensions: + type: object + properties: + extensions: + type: array + items: + $ref: '#/components/schemas/Extension' + Extension: + allOf: + - $ref: '#/components/schemas/HasSemantics' + - properties: + name: + type: string + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + value: + type: string + refersTo: + $ref: '#/components/schemas/Reference' + required: + - name + AssetAdministrationShell: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - properties: + derivedFrom: + $ref: '#/components/schemas/Reference' + assetInformation: + $ref: '#/components/schemas/AssetInformation' + submodels: + type: array + items: + $ref: '#/components/schemas/Reference' + views: + type: array + items: + $ref: '#/components/schemas/View' + security: + $ref: '#/components/schemas/Security' + required: + - assetInformation + Identifier: + type: object + properties: + id: + type: string + idType: + $ref: '#/components/schemas/KeyType' + required: + - id + - idType + KeyType: + type: string + enum: + - Custom + - IRDI + - IRI + - IdShort + - FragmentId + AdministrativeInformation: + type: object + properties: + version: + type: string + revision: + type: string + LangString: + type: object + properties: + language: + type: string + text: + type: string + required: + - language + - text + Reference: + type: object + properties: + keys: + type: array + items: + $ref: '#/components/schemas/Key' + required: + - keys + Key: + type: object + properties: + type: + $ref: '#/components/schemas/KeyElements' + idType: + $ref: '#/components/schemas/KeyType' + value: + type: string + required: + - type + - idType + - value + KeyElements: + type: string + enum: + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference + ModelTypes: + type: string + enum: + - Asset + - AssetAdministrationShell + - ConceptDescription + - Submodel + - AccessPermissionRule + - AnnotatedRelationshipElement + - BasicEvent + - Blob + - Capability + - DataElement + - File + - Entity + - Event + - MultiLanguageProperty + - Operation + - Property + - Range + - ReferenceElement + - RelationshipElement + - SubmodelElement + - SubmodelElementCollection + - View + - GlobalReference + - FragmentReference + - Constraint + - Formula + - Qualifier + ModelType: + type: object + properties: + name: + $ref: '#/components/schemas/ModelTypes' + required: + - name + EmbeddedDataSpecification: + type: object + properties: + dataSpecification: + $ref: '#/components/schemas/Reference' + dataSpecificationContent: + $ref: '#/components/schemas/DataSpecificationContent' + required: + - dataSpecification + - dataSpecificationContent + DataSpecificationContent: + oneOf: + - $ref: '#/components/schemas/DataSpecificationIEC61360Content' + - $ref: '#/components/schemas/DataSpecificationPhysicalUnitContent' + DataSpecificationPhysicalUnitContent: + type: object + properties: + unitName: + type: string + unitSymbol: + type: string + definition: + type: array + items: + $ref: '#/components/schemas/LangString' + siNotation: + type: string + siName: + type: string + dinNotation: + type: string + eceName: + type: string + eceCode: + type: string + nistName: + type: string + sourceOfDefinition: + type: string + conversionFactor: + type: string + registrationAuthorityId: + type: string + supplier: + type: string + required: + - unitName + - unitSymbol + - definition + DataSpecificationIEC61360Content: + allOf: + - $ref: '#/components/schemas/ValueObject' + - type: object + properties: + dataType: + enum: + - DATE + - STRING + - STRING_TRANSLATABLE + - REAL_MEASURE + - REAL_COUNT + - REAL_CURRENCY + - BOOLEAN + - URL + - RATIONAL + - RATIONAL_MEASURE + - TIME + - TIMESTAMP + - INTEGER_COUNT + - INTEGER_MEASURE + - INTEGER_CURRENCY + definition: + type: array + items: + $ref: '#/components/schemas/LangString' + preferredName: + type: array + items: + $ref: '#/components/schemas/LangString' + shortName: + type: array + items: + $ref: '#/components/schemas/LangString' + sourceOfDefinition: + type: string + symbol: + type: string + unit: + type: string + unitId: + $ref: '#/components/schemas/Reference' + valueFormat: + type: string + valueList: + $ref: '#/components/schemas/ValueList' + levelType: + type: array + items: + $ref: '#/components/schemas/LevelType' + required: + - preferredName + LevelType: + type: string + enum: + - Min + - Max + - Nom + - Typ + ValueList: + type: object + properties: + valueReferencePairTypes: + type: array + minItems: 1 + items: + $ref: '#/components/schemas/ValueReferencePairType' + required: + - valueReferencePairTypes + ValueReferencePairType: + allOf: + - $ref: '#/components/schemas/ValueObject' + ValueObject: + type: object + properties: + value: + type: string + valueId: + $ref: '#/components/schemas/Reference' + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + Asset: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + AssetInformation: + allOf: + - properties: + assetKind: + $ref: '#/components/schemas/AssetKind' + globalAssetId: + $ref: '#/components/schemas/Reference' + externalAssetIds: + type: array + items: + $ref: '#/components/schemas/IdentifierKeyValuePair' + billOfMaterial: + type: array + items: + $ref: '#/components/schemas/Reference' + thumbnail: + $ref: '#/components/schemas/File' + required: + - assetKind + IdentifierKeyValuePair: + allOf: + - $ref: '#/components/schemas/HasSemantics' + - properties: + key: + type: string + value: + type: string + subjectId: + $ref: '#/components/schemas/Reference' + required: + - key + - value + - subjectId + AssetKind: + type: string + enum: + - Type + - Instance + ModelingKind: + type: string + enum: + - Template + - Instance + Submodel: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/Qualifiable' + - $ref: '#/components/schemas/HasSemantics' + - properties: + kind: + $ref: '#/components/schemas/ModelingKind' + submodelElements: + type: array + items: + $ref: '#/components/schemas/SubmodelElement' + Constraint: + type: object + properties: + modelType: + $ref: '#/components/schemas/ModelType' + required: + - modelType + Operation: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + inputVariable: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + outputVariable: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + inoutputVariable: + type: array + items: + $ref: '#/components/schemas/OperationVariable' + OperationVariable: + type: object + properties: + value: + oneOf: + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/Capability' + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Event' + - $ref: '#/components/schemas/BasicEvent' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Operation' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + - $ref: '#/components/schemas/RelationshipElement' + - $ref: '#/components/schemas/SubmodelElementCollection' + required: + - value + SubmodelElement: + allOf: + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/HasSemantics' + - $ref: '#/components/schemas/Qualifiable' + - properties: + kind: + $ref: '#/components/schemas/ModelingKind' + idShort: + type: string + required: + - idShort + Event: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + BasicEvent: + allOf: + - $ref: '#/components/schemas/Event' + - properties: + observed: + $ref: '#/components/schemas/Reference' + required: + - observed + EntityType: + type: string + enum: + - CoManagedEntity + - SelfManagedEntity + Entity: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + statements: + type: array + items: + $ref: '#/components/schemas/SubmodelElement' + entityType: + $ref: '#/components/schemas/EntityType' + globalAssetId: + $ref: '#/components/schemas/Reference' + specificAssetIds: + $ref: '#/components/schemas/IdentifierKeyValuePair' + required: + - entityType + View: + allOf: + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/HasDataSpecification' + - $ref: '#/components/schemas/HasSemantics' + - properties: + containedElements: + type: array + items: + $ref: '#/components/schemas/Reference' + ConceptDescription: + allOf: + - $ref: '#/components/schemas/Identifiable' + - $ref: '#/components/schemas/HasDataSpecification' + - properties: + isCaseOf: + type: array + items: + $ref: '#/components/schemas/Reference' + Capability: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + Property: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - $ref: '#/components/schemas/ValueObject' + Range: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + valueType: + type: string + enum: + - anyUri + - base64Binary + - boolean + - date + - dateTime + - dateTimeStamp + - decimal + - integer + - long + - int + - short + - byte + - nonNegativeInteger + - positiveInteger + - unsignedLong + - unsignedInt + - unsignedShort + - unsignedByte + - nonPositiveInteger + - negativeInteger + - double + - duration + - dayTimeDuration + - yearMonthDuration + - float + - gDay + - gMonth + - gMonthDay + - gYear + - gYearMonth + - hexBinary + - NOTATION + - QName + - string + - normalizedString + - token + - language + - Name + - NCName + - ENTITY + - ID + - IDREF + - NMTOKEN + - time + min: + type: string + max: + type: string + required: + - valueType + MultiLanguageProperty: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: array + items: + $ref: '#/components/schemas/LangString' + valueId: + $ref: '#/components/schemas/Reference' + File: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + Blob: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: string + mimeType: + type: string + required: + - mimeType + ReferenceElement: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + $ref: '#/components/schemas/Reference' + SubmodelElementCollection: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + value: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/Capability' + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Event' + - $ref: '#/components/schemas/BasicEvent' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Operation' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + - $ref: '#/components/schemas/RelationshipElement' + - $ref: '#/components/schemas/SubmodelElementCollection' + allowDuplicates: + type: boolean + ordered: + type: boolean + RelationshipElement: + allOf: + - $ref: '#/components/schemas/SubmodelElement' + - properties: + first: + $ref: '#/components/schemas/Reference' + second: + $ref: '#/components/schemas/Reference' + required: + - first + - second + AnnotatedRelationshipElement: + allOf: + - $ref: '#/components/schemas/RelationshipElement' + - properties: + annotation: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Blob' + - $ref: '#/components/schemas/File' + - $ref: '#/components/schemas/MultiLanguageProperty' + - $ref: '#/components/schemas/Property' + - $ref: '#/components/schemas/Range' + - $ref: '#/components/schemas/ReferenceElement' + Qualifier: + allOf: + - $ref: '#/components/schemas/Constraint' + - $ref: '#/components/schemas/HasSemantics' + - $ref: '#/components/schemas/ValueObject' + - properties: + type: + type: string + required: + - type + Formula: + allOf: + - $ref: '#/components/schemas/Constraint' + - properties: + dependsOn: + type: array + items: + $ref: '#/components/schemas/Reference' + Security: + type: object + properties: + accessControlPolicyPoints: + $ref: '#/components/schemas/AccessControlPolicyPoints' + certificate: + type: array + items: + oneOf: + - $ref: '#/components/schemas/BlobCertificate' + requiredCertificateExtension: + type: array + items: + $ref: '#/components/schemas/Reference' + required: + - accessControlPolicyPoints + Certificate: + type: object + BlobCertificate: + allOf: + - $ref: '#/components/schemas/Certificate' + - properties: + blobCertificate: + $ref: '#/components/schemas/Blob' + containedExtension: + type: array + items: + $ref: '#/components/schemas/Reference' + lastCertificate: + type: boolean + AccessControlPolicyPoints: + type: object + properties: + policyAdministrationPoint: + $ref: '#/components/schemas/PolicyAdministrationPoint' + policyDecisionPoint: + $ref: '#/components/schemas/PolicyDecisionPoint' + policyEnforcementPoint: + $ref: '#/components/schemas/PolicyEnforcementPoint' + policyInformationPoints: + $ref: '#/components/schemas/PolicyInformationPoints' + required: + - policyAdministrationPoint + - policyDecisionPoint + - policyEnforcementPoint + PolicyAdministrationPoint: + type: object + properties: + localAccessControl: + $ref: '#/components/schemas/AccessControl' + externalAccessControl: + type: boolean + required: + - externalAccessControl + PolicyInformationPoints: + type: object + properties: + internalInformationPoint: + type: array + items: + $ref: '#/components/schemas/Reference' + externalInformationPoint: + type: boolean + required: + - externalInformationPoint + PolicyEnforcementPoint: + type: object + properties: + externalPolicyEnforcementPoint: + type: boolean + required: + - externalPolicyEnforcementPoint + PolicyDecisionPoint: + type: object + properties: + externalPolicyDecisionPoints: + type: boolean + required: + - externalPolicyDecisionPoints + AccessControl: + type: object + properties: + selectableSubjectAttributes: + $ref: '#/components/schemas/Reference' + defaultSubjectAttributes: + $ref: '#/components/schemas/Reference' + selectablePermissions: + $ref: '#/components/schemas/Reference' + defaultPermissions: + $ref: '#/components/schemas/Reference' + selectableEnvironmentAttributes: + $ref: '#/components/schemas/Reference' + defaultEnvironmentAttributes: + $ref: '#/components/schemas/Reference' + accessPermissionRule: + type: array + items: + $ref: '#/components/schemas/AccessPermissionRule' + AccessPermissionRule: + allOf: + - $ref: '#/components/schemas/Referable' + - $ref: '#/components/schemas/Qualifiable' + - properties: + targetSubjectAttributes: + type: array + items: + $ref: '#/components/schemas/SubjectAttributes' + minItems: 1 + permissionsPerObject: + type: array + items: + $ref: '#/components/schemas/PermissionsPerObject' + required: + - targetSubjectAttributes + SubjectAttributes: + type: object + properties: + subjectAttributes: + type: array + items: + $ref: '#/components/schemas/Reference' + minItems: 1 + PermissionsPerObject: + type: object + properties: + object: + $ref: '#/components/schemas/Reference' + targetObjectAttributes: + $ref: '#/components/schemas/ObjectAttributes' + permission: + type: array + items: + $ref: '#/components/schemas/Permission' + ObjectAttributes: + type: object + properties: + objectAttribute: + type: array + items: + $ref: '#/components/schemas/Property' + minItems: 1 + Permission: + type: object + properties: + permission: + $ref: '#/components/schemas/Reference' + kindOfPermission: + type: string + enum: + - Allow + - Deny + - NotApplicable + - Undefined + required: + - permission + - kindOfPermission diff --git a/test/adapter/http-api-oas.yaml b/test/adapter/http-api-oas-submodel.yaml similarity index 83% rename from test/adapter/http-api-oas.yaml rename to test/adapter/http-api-oas-submodel.yaml index 13c4199b7..6afd7ddc4 100644 --- a/test/adapter/http-api-oas.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -3,23 +3,12 @@ info: version: "1" title: PyI40AAS REST API description: "REST API Specification for the [PyI40AAS framework](https://git.rwth-aachen.de/acplt/pyi40aas). - - - The git repository of this document is available [here](https://git.rwth-aachen.de/leon.moeller/pyi40aas-oas). - --- + **Submodel Interface** - **General:** - - Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`. - - - This specification contains one exemplary `PATCH` route. In the future there should be a `PATCH` route for every `PUT` route, where it makes sense. - - - In our implementation the AAS Interface is available at `/aas/{identifier}` and the Submodel Interface at `/submodel/{identifier}`." + Any identifier/identification objects are encoded as follows `{identifierType}:URIencode(URIencode({identifier}))`, e.g. `IRI:http:%252F%252Facplt.org%252Fasset`." contact: name: "Michael Thies, Torben Miny, Leon Möller" license: @@ -40,331 +29,6 @@ servers: description: The Version of the API-Specification paths: "/": - get: - summary: Retrieves the stripped AssetAdministrationShell, without Submodel-References and Views. - operationId: ReadAAS - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/AASResult" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/AASResult" - tags: - - Asset Administration Shell Interface - "/submodels": - get: - summary: Returns all Submodel-References of the AssetAdministrationShell - operationId: ReadAASSubmodelReferences - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceListResult" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceListResult" - tags: - - Asset Administration Shell Interface - post: - summary: Adds a new Submodel-Reference to the AssetAdministrationShell - operationId: CreateAASSubmodelReference - requestBody: - description: The Submodel-Reference to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/Reference" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - headers: - Location: - description: The URL of the created Submodel-Reference - schema: - type: string - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - "409": - description: Submodel-Reference already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - "422": - description: Request body is not a Reference or not resolvable - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - tags: - - Asset Administration Shell Interface - "/submodels/{submodel-identifier}": - parameters: - - name: submodel-identifier - in: path - description: The Identifier of the referenced Submodel - required: true - schema: - type: string - get: - summary: Returns the Reference specified by submodel-identifier - operationId: ReadAASSubmodelReference - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - "404": - description: AssetAdministrationShell not found or the specified Submodel is not referenced - content: - "application/json": - schema: - $ref: "#/components/schemas/ReferenceResult" - tags: - - Asset Administration Shell Interface - delete: - summary: Deletes the Reference specified by submodel-identifier - operationId: DeleteAASSubmodelReference - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: AssetAdministrationShell not found or the specified Submodel is not referenced - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - "/views": - get: - summary: Returns all Views of the AssetAdministrationShell - operationId: ReadAASViews - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewListResult" - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewListResult" - tags: - - Asset Administration Shell Interface - post: - summary: Adds a new View to the AssetAdministrationShell - operationId: CreateAASView - requestBody: - description: The View to create - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "201": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The URL of the created View - schema: - type: string - "404": - description: AssetAdministrationShell not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "409": - description: View with same idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface - "/views/{view-idShort}": - parameters: - - name: view-idShort - in: path - description: The idShort of the View - required: true - schema: - type: string - get: - summary: Returns a specific View of the AssetAdministrationShell - operationId: ReadAASView - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface - put: - summary: Updates a specific View of the AssetAdministrationShell - operationId: UpdateAASView - requestBody: - description: The View used to overwrite the existing View - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "201": - description: Success (idShort changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The new URL of the View - schema: - type: string - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "409": - description: idShort changed and new idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface - patch: - summary: Updates a specific View of the AssetAdministrationShell by only providing properties that should be changed - operationId: PatchAASView - requestBody: - description: The (partial) View used to update the existing View - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "201": - description: Success (idShort changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The new URL of the View - schema: - type: string - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "409": - description: idShort changed and new idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface - delete: - summary: Deletes a specific View from the Asset Administration Shell - operationId: DeleteAASView - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/BaseResult" - tags: - - Asset Administration Shell Interface - "//": get: summary: "Returns the stripped Submodel (without SubmodelElements and Constraints (property: qualifiers))" operationId: ReadSubmodel From 8151c61f69d47878561007bc19a2ce4fc099ecd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 22:43:32 +0200 Subject: [PATCH 060/157] adapter.http: make trailing slashes the default http-api-oas: follow suit --- basyx/aas/adapter/http.py | 73 +++++++++++-------------- test/adapter/http-api-oas-aas.yaml | 8 +-- test/adapter/http-api-oas-submodel.yaml | 18 +++--- 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 97432daf5..09f0a584b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -327,26 +327,26 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("/", methods=["GET"], + Rule("//", methods=["GET"], endpoint=self.get_aas_submodel_refs_specific), - Rule("/", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]), Submount("/views", [ Rule("/", methods=["GET"], endpoint=self.get_aas_views), Rule("/", methods=["POST"], endpoint=self.post_aas_views), - Rule("/", methods=["GET"], + Rule("//", methods=["GET"], endpoint=self.get_aas_views_specific), - Rule("/", methods=["PUT"], + Rule("//", methods=["PUT"], endpoint=self.put_aas_views_specific), - Rule("/", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_views_specific) ]) ]), Submount("/submodels/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/submodelElements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/submodelElements", methods=["POST"], endpoint=self.post_submodel_submodel_elements), + Rule("/submodelElements/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodelElements/", methods=["POST"], endpoint=self.post_submodel_submodel_elements), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_specific_nested), @@ -356,49 +356,52 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.delete_submodel_submodel_elements_specific_nested), # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - Rule("/values", methods=["GET"], + Rule("/values/", methods=["GET"], endpoint=self.factory_get_submodel_submodel_elements_nested_attr( model.SubmodelElementCollection, "value")), # type: ignore - Rule("/values", methods=["POST"], + Rule("/values/", methods=["POST"], endpoint=self.factory_post_submodel_submodel_elements_nested_attr( model.SubmodelElementCollection, "value")), # type: ignore - Rule("/annotations", methods=["GET"], + Rule("/annotations/", methods=["GET"], endpoint=self.factory_get_submodel_submodel_elements_nested_attr( model.AnnotatedRelationshipElement, "annotation")), - Rule("/annotations", methods=["POST"], + Rule("/annotations/", methods=["POST"], endpoint=self.factory_post_submodel_submodel_elements_nested_attr( model.AnnotatedRelationshipElement, "annotation", request_body_type=model.DataElement)), # type: ignore - Rule("/statements", methods=["GET"], + Rule("/statements/", methods=["GET"], endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, "statement")), - Rule("/statements", methods=["POST"], + Rule("/statements/", methods=["POST"], endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, "statement")), - Rule("/constraints", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/constraints", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["GET"], + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) + ]), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["PUT"], + Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_constraints), - ]), - Rule("/constraints", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/constraints", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("/constraints/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints) + ]) ]) ]) ], converters={ "identifier": IdentifierConverter, "id_short_path": IdShortPathConverter - }, strict_slashes=False) + }) def __call__(self, environ, start_response): response = self.handle_request(Request(environ)) @@ -465,18 +468,6 @@ def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: - # redirect requests with a trailing slash to the path without trailing slash - # if the path without trailing slash exists. - # if not, map_adapter.match() will raise NotFound() in both cases - if request.path != "/" and request.path.endswith("/"): - map_adapter.match(request.path[:-1], request.method) - # from werkzeug's internal routing redirection - raise werkzeug.routing.RequestRedirect( - map_adapter.make_redirect_url( - werkzeug.urls.url_quote(request.path[:-1], map_adapter.map.charset, safe="/:|+"), - map_adapter.query_args - ) - ) endpoint, values = map_adapter.match() if endpoint is None: raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index 2d6b06276..620bc684c 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -47,7 +47,7 @@ paths: $ref: "#/components/schemas/AASResult" tags: - Asset Administration Shell Interface - "/submodels": + "/submodels/": get: summary: Returns all Submodel-References of the AssetAdministrationShell operationId: ReadAASSubmodelReferences @@ -108,7 +108,7 @@ paths: $ref: "#/components/schemas/ReferenceResult" tags: - Asset Administration Shell Interface - "/submodels/{submodel-identifier}": + "/submodels/{submodel-identifier}/": parameters: - name: submodel-identifier in: path @@ -152,7 +152,7 @@ paths: $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface - "/views": + "/views/": get: summary: Returns all Views of the AssetAdministrationShell operationId: ReadAASViews @@ -213,7 +213,7 @@ paths: $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface - "/views/{view-idShort}": + "/views/{view-idShort}/": parameters: - name: view-idShort in: path diff --git a/test/adapter/http-api-oas-submodel.yaml b/test/adapter/http-api-oas-submodel.yaml index 6afd7ddc4..0e270fd82 100644 --- a/test/adapter/http-api-oas-submodel.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -47,7 +47,7 @@ paths: $ref: "#/components/schemas/SubmodelResult" tags: - Submodel Interface - "/constraints": + "/constraints/": get: summary: Returns all Constraints of the current Submodel operationId: ReadSubmodelConstraints @@ -108,7 +108,7 @@ paths: $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface - "/constraints/{qualifier-type}": + "/constraints/{qualifier-type}/": parameters: - name: qualifier-type in: path @@ -200,7 +200,7 @@ paths: $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface - "/submodelElements": + "/submodelElements/": get: summary: Returns all SubmodelElements of the current Submodel operationId: ReadSubmodelSubmodelElements @@ -261,7 +261,7 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{idShort-path}": + "/{idShort-path}/": parameters: - name: idShort-path in: path @@ -353,7 +353,7 @@ paths: $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface - "/{idShort-path}/value": + "/{idShort-path}/value/": parameters: - name: idShort-path in: path @@ -433,7 +433,7 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{idShort-path}/annotation": + "/{idShort-path}/annotation/": parameters: - name: idShort-path in: path @@ -513,7 +513,7 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{idShort-path}/statement": + "/{idShort-path}/statement/": parameters: - name: idShort-path in: path @@ -593,7 +593,7 @@ paths: $ref: "#/components/schemas/SubmodelElementResult" tags: - Submodel Interface - "/{idShort-path}/constraints": + "/{idShort-path}/constraints/": parameters: - name: idShort-path in: path @@ -661,7 +661,7 @@ paths: $ref: "#/components/schemas/ConstraintResult" tags: - Submodel Interface - "/{idShort-path}/constraints/{qualifier-type}": + "/{idShort-path}/constraints/{qualifier-type}/": parameters: - name: idShort-path in: path From 538de8cd9d9fa2a4b2e985f6e71fd7a5c1a349a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:03:53 +0200 Subject: [PATCH 061/157] adapter.http: return json if Accept header is missing or empty --- basyx/aas/adapter/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 09f0a584b..c3d7db968 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -192,6 +192,8 @@ def get_response_type(request: Request) -> Type[APIResponse]: "application/xml": XmlResponse, "text/xml": XmlResponseAlt } + if len(request.accept_mimetypes) == 0: + return JsonResponse mime_type = request.accept_mimetypes.best_match(response_types) if mime_type is None: raise werkzeug.exceptions.NotAcceptable(f"This server supports the following content types: " From e42212bba7a1cb34e7f7896e9f2a07f52e80a162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:07:41 +0200 Subject: [PATCH 062/157] adapter.http: get root cause only for xml deserializer errors --- basyx/aas/adapter/http.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index c3d7db968..46e0689ec 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -253,11 +253,17 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: if expect_type is model.AASReference: rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) else: - xml_data = io.BytesIO(request.get_data()) - rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) - except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError) as e: - while e.__cause__ is not None: - e = e.__cause__ + try: + xml_data = io.BytesIO(request.get_data()) + rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) + except (KeyError, ValueError) as e: + # xml deserialization creates an error chain. since we only return one error, return the root cause + f = e + while f.__cause__ is not None: + f = f.__cause__ + raise f from e + except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError, model.AASConstraintViolation) \ + as e: raise UnprocessableEntity(str(e)) from e assert isinstance(rv, expect_type) From 3c91f64ac394d976ad73679f2408e7b3aae3d74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:08:49 +0200 Subject: [PATCH 063/157] adapter.http: return 422 if parsed object doesn't match the expected type --- basyx/aas/adapter/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 46e0689ec..0a9e81889 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -266,7 +266,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: as e: raise UnprocessableEntity(str(e)) from e - assert isinstance(rv, expect_type) + if not isinstance(rv, expect_type): + raise UnprocessableEntity(f"Object {rv!r} is not of type {expect_type.__name__}!") return rv From 1259203aec7b7577426b0d18f251462dfb9aaf0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:09:59 +0200 Subject: [PATCH 064/157] adapter.http: fix existance checks --- basyx/aas/adapter/http.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 0a9e81889..7605de96f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -558,7 +558,7 @@ def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapt aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() view = parse_request_body(request, model.View) - if view.id_short in aas.view: + if ("id_short", view.id_short) in aas.view: raise Conflict(f"View with id_short {view.id_short} already exists!") aas.view.add(view) aas.commit() @@ -604,7 +604,7 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] - if view_idshort not in aas.view: + if ("id_short", view_idshort) not in aas.view: raise NotFound(f"No view with id_short {view_idshort} found!") aas.view.remove(view_idshort) return response_t(Result(None)) @@ -630,7 +630,7 @@ def post_submodel_submodel_elements(self, request: Request, url_args: Dict, map_ # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - if submodel_element.id_short in submodel.submodel_element: + if ("id_short", submodel_element.id_short) in submodel.submodel_element: raise Conflict(f"Submodel element with id_short {submodel_element.id_short} already exists!") submodel.submodel_element.add(submodel_element) submodel.commit() @@ -740,7 +740,6 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: raise UnprocessableEntity(f"Type of new qualifier {new_qualifier} doesn't not match " f"the current submodel element {qualifier}") qualifier_type_changed = qualifier_type != new_qualifier.type - # TODO: have to pass a tuple to __contains__ here. can't this be simplified? if qualifier_type_changed and ("type", new_qualifier.type) in sm_or_se.qualifier: raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") sm_or_se.remove_qualifier_by_type(qualifier.type) @@ -798,7 +797,7 @@ def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response if not isinstance(submodel_element, type_): raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") new_submodel_element = parse_request_body(request, request_body_type) - if new_submodel_element.id_short in getattr(submodel_element, attr): + if ("id_short", new_submodel_element.id_short) in getattr(submodel_element, attr): raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") getattr(submodel_element, attr).add(new_submodel_element) submodel_element.commit() From d821da60a0285e9d26a8bca354af26b8eacd31b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:20:00 +0200 Subject: [PATCH 065/157] http-api-oas: remove PATCH route --- test/adapter/http-api-oas-aas.yaml | 48 ------------------------------ 1 file changed, 48 deletions(-) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index 620bc684c..dee73ab7d 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -287,54 +287,6 @@ paths: $ref: "#/components/schemas/ViewResult" tags: - Asset Administration Shell Interface - patch: - summary: Updates a specific View of the AssetAdministrationShell by only providing properties that should be changed - operationId: PatchAASView - requestBody: - description: The (partial) View used to update the existing View - required: true - content: - "application/json": - schema: - $ref: "#/components/schemas/View" - responses: - "200": - description: Success - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "201": - description: Success (idShort changed) - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - headers: - Location: - description: The new URL of the View - schema: - type: string - "404": - description: AssetAdministrationShell or View not found - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "409": - description: idShort changed and new idShort already exists - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - "422": - description: Request body is not a valid View - content: - "application/json": - schema: - $ref: "#/components/schemas/ViewResult" - tags: - - Asset Administration Shell Interface delete: summary: Deletes a specific View from the Asset Administration Shell operationId: DeleteAASView From 3737e8f86d67604d8abac7bb3208a894a042d57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:33:04 +0200 Subject: [PATCH 066/157] http-api-oas: make result types more accurate --- test/adapter/http-api-oas-aas.yaml | 56 +++++++----- test/adapter/http-api-oas-submodel.yaml | 114 ++++++++++++++---------- 2 files changed, 101 insertions(+), 69 deletions(-) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index dee73ab7d..ba880a088 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -38,13 +38,13 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/AASResult" + $ref: "#/components/schemas/AssetAdministrationShellResult" "404": description: AssetAdministrationShell not found content: "application/json": schema: - $ref: "#/components/schemas/AASResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface "/submodels/": @@ -63,7 +63,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ReferenceListResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface post: @@ -93,19 +93,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ReferenceResult" + $ref: "#/components/schemas/BaseResult" "409": description: Submodel-Reference already exists content: "application/json": schema: - $ref: "#/components/schemas/ReferenceResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a Reference or not resolvable content: "application/json": schema: - $ref: "#/components/schemas/ReferenceResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface "/submodels/{submodel-identifier}/": @@ -131,7 +131,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ReferenceResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface delete: @@ -168,7 +168,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ViewListResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface post: @@ -198,19 +198,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" "409": description: View with same idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid View content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface "/views/{view-idShort}/": @@ -236,7 +236,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface put: @@ -272,19 +272,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" "409": description: idShort changed and new idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid View content: "application/json": schema: - $ref: "#/components/schemas/ViewResult" + $ref: "#/components/schemas/BaseResult" tags: - Asset Administration Shell Interface delete: @@ -312,11 +312,9 @@ components: properties: success: type: boolean - readOnly: true error: type: object nullable: true - readOnly: true properties: type: enum: @@ -334,20 +332,22 @@ components: type: string data: nullable: true - readOnly: true - type: object - AASResult: + AssetAdministrationShellResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedAssetAdministrationShell" + error: + nullable: true ReferenceResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/Reference" + error: + nullable: true ReferenceListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -356,12 +356,16 @@ components: type: array items: $ref: "#/components/schemas/Reference" + error: + nullable: true ViewResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/View" + error: + nullable: true ViewListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -370,18 +374,24 @@ components: type: array items: $ref: "#/components/schemas/View" + error: + nullable: true SubmodelResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedSubmodel" + error: + nullable: true SubmodelElementResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedSubmodelElement" + error: + nullable: true SubmodelElementListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -390,12 +400,16 @@ components: type: array items: $ref: "#/components/schemas/StrippedSubmodelElement" + error: + nullable: true ConstraintResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/Constraint" + error: + nullable: true ConstraintListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -404,6 +418,8 @@ components: type: array items: $ref: "#/components/schemas/Constraint" + error: + nullable: true StrippedAssetAdministrationShell: allOf: - $ref: "#/components/schemas/AssetAdministrationShell" diff --git a/test/adapter/http-api-oas-submodel.yaml b/test/adapter/http-api-oas-submodel.yaml index 0e270fd82..6b8592e0c 100644 --- a/test/adapter/http-api-oas-submodel.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -44,7 +44,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/constraints/": @@ -63,7 +63,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -93,19 +93,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "409": description: "When trying to add a qualifier: Qualifier with same type already exists" content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid Qualifier content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/constraints/{qualifier-type}/": @@ -131,7 +131,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface put: @@ -167,19 +167,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "409": description: type changed and new type already exists content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid Qualifier content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface delete: @@ -216,7 +216,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -246,19 +246,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: SubmodelElement with same idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/": @@ -284,7 +284,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface put: @@ -320,19 +320,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: idShort changed and new idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement **or** the type of the new SubmodelElement differs from the existing one content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface delete: @@ -370,19 +370,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" "400": description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -412,25 +412,25 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: SubmodelElement with same idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/annotation/": @@ -456,13 +456,13 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -492,25 +492,25 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: SubmodelElement with given idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/statement/": @@ -536,13 +536,13 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -572,25 +572,25 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "409": description: SubmodelElement with same idShort already exists content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid SubmodelElement content: "application/json": schema: - $ref: "#/components/schemas/SubmodelElementResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/constraints/": @@ -616,7 +616,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintListResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface post: @@ -646,19 +646,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "409": description: "When trying to add a qualifier: Qualifier with specified type already exists" content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid Qualifier content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface "/{idShort-path}/constraints/{qualifier-type}/": @@ -690,7 +690,7 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface put: @@ -726,19 +726,19 @@ paths: content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "409": description: type changed and new type already exists content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" "422": description: Request body is not a valid Qualifier content: "application/json": schema: - $ref: "#/components/schemas/ConstraintResult" + $ref: "#/components/schemas/BaseResult" tags: - Submodel Interface delete: @@ -766,11 +766,9 @@ components: properties: success: type: boolean - readOnly: true error: type: object nullable: true - readOnly: true properties: type: enum: @@ -788,20 +786,22 @@ components: type: string data: nullable: true - readOnly: true - type: object - AASResult: + AssetAdministrationShellResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedAssetAdministrationShell" + error: + nullable: true ReferenceResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/Reference" + error: + nullable: true ReferenceListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -810,12 +810,16 @@ components: type: array items: $ref: "#/components/schemas/Reference" + error: + nullable: true ViewResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/View" + error: + nullable: true ViewListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -824,18 +828,24 @@ components: type: array items: $ref: "#/components/schemas/View" + error: + nullable: true SubmodelResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedSubmodel" + error: + nullable: true SubmodelElementResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/StrippedSubmodelElement" + error: + nullable: true SubmodelElementListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -844,12 +854,16 @@ components: type: array items: $ref: "#/components/schemas/StrippedSubmodelElement" + error: + nullable: true ConstraintResult: allOf: - $ref: "#/components/schemas/BaseResult" - properties: data: $ref: "#/components/schemas/Constraint" + error: + nullable: true ConstraintListResult: allOf: - $ref: "#/components/schemas/BaseResult" @@ -858,6 +872,8 @@ components: type: array items: $ref: "#/components/schemas/Constraint" + error: + nullable: true StrippedAssetAdministrationShell: allOf: - $ref: "#/components/schemas/AssetAdministrationShell" From cc18322ff37dee91fa625b311174cd4f940d793a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 29 Apr 2021 23:47:40 +0200 Subject: [PATCH 067/157] adapter.http: add missing type annotation --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7605de96f..471cb0f5c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -258,7 +258,7 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) except (KeyError, ValueError) as e: # xml deserialization creates an error chain. since we only return one error, return the root cause - f = e + f: BaseException = e while f.__cause__ is not None: f = f.__cause__ raise f from e From 40da58f85d2b84584b11cfa0fe079b90447f724d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Jul 2021 16:27:26 +0200 Subject: [PATCH 068/157] adapter.http: fix id_short validation --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 471cb0f5c..392ce3ad3 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -300,7 +300,7 @@ def to_python(self, value: str) -> model.Identifier: def validate_id_short(id_short: str) -> bool: try: model.MultiLanguageProperty(id_short) - except ValueError: + except model.AASConstraintViolation: return False return True From 06ca48d86d9bfa7f58a589105f403f6fc82c9aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Jul 2021 16:27:55 +0200 Subject: [PATCH 069/157] adapter.http: fix view removal --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 392ce3ad3..95f768b71 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -606,7 +606,7 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) view_idshort = url_args["view_idshort"] if ("id_short", view_idshort) not in aas.view: raise NotFound(f"No view with id_short {view_idshort} found!") - aas.view.remove(view_idshort) + aas.view.remove(("id_short", view_idshort)) return response_t(Result(None)) # --------- SUBMODEL ROUTES --------- From ff99b7fe9e8a962006ee9999d051d97b340dbed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Jul 2021 17:39:34 +0200 Subject: [PATCH 070/157] http-api-oas: add missing 400 responses and fix descriptions --- test/adapter/http-api-oas-aas.yaml | 12 +++++ test/adapter/http-api-oas-submodel.yaml | 60 ++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index ba880a088..d63166036 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -126,6 +126,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ReferenceResult" + "400": + description: Invalid submodel-identifier format + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: AssetAdministrationShell not found or the specified Submodel is not referenced content: @@ -144,6 +150,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/BaseResult" + "400": + description: Invalid submodel-identifier format + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: AssetAdministrationShell not found or the specified Submodel is not referenced content: diff --git a/test/adapter/http-api-oas-submodel.yaml b/test/adapter/http-api-oas-submodel.yaml index 6b8592e0c..fa1f43cb6 100644 --- a/test/adapter/http-api-oas-submodel.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -279,6 +279,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/SubmodelElementListResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -315,6 +321,12 @@ paths: description: The new URL of the SubmodelElement schema: type: string + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -345,6 +357,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/BaseResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -372,7 +390,7 @@ paths: schema: $ref: "#/components/schemas/BaseResult" "400": - description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible + description: Invalid idShort or SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: "application/json": schema: @@ -408,7 +426,7 @@ paths: schema: type: string "400": - description: SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible + description: Invalid idShort or SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: "application/json": schema: @@ -452,7 +470,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "400": - description: SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible + description: Invalid idShort or SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible content: "application/json": schema: @@ -488,7 +506,7 @@ paths: schema: type: string "400": - description: SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible + description: Invalid idShort or SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible content: "application/json": schema: @@ -532,7 +550,7 @@ paths: schema: $ref: "#/components/schemas/SubmodelElementListResult" "400": - description: SubmodelElement exists, but is not an Entity, so /statement is not possible. + description: Invalid idShort or SubmodelElement exists, but is not an Entity, so /statement is not possible. content: "application/json": schema: @@ -568,7 +586,7 @@ paths: schema: type: string "400": - description: SubmodelElement exists, but is not an Entity, so /statement is not possible + description: Invalid idShort or SubmodelElement exists, but is not an Entity, so /statement is not possible content: "application/json": schema: @@ -611,6 +629,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintListResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -641,6 +665,12 @@ paths: description: The URL of the created Constraint schema: type: string + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel or any SubmodelElement referred by idShort-path not found content: @@ -685,6 +715,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: @@ -721,6 +757,12 @@ paths: description: The new URL of the Qualifier schema: type: string + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: @@ -751,6 +793,12 @@ paths: "application/json": schema: $ref: "#/components/schemas/BaseResult" + "400": + description: Invalid idShort + content: + "application/json": + schema: + $ref: "#/components/schemas/BaseResult" "404": description: Submodel, Qualifier or any SubmodelElement referred by idShort-path not found content: From 826e80acfb22ac8ba3e622bfad5fadd78271f2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 7 Jul 2021 17:41:53 +0200 Subject: [PATCH 071/157] http-api-oas: add links --- test/adapter/http-api-oas-aas.yaml | 42 +++++++ test/adapter/http-api-oas-submodel.yaml | 148 ++++++++++++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/test/adapter/http-api-oas-aas.yaml b/test/adapter/http-api-oas-aas.yaml index d63166036..27d029373 100644 --- a/test/adapter/http-api-oas-aas.yaml +++ b/test/adapter/http-api-oas-aas.yaml @@ -205,6 +205,13 @@ paths: description: The URL of the created View schema: type: string + links: + ReadAASViewByIdShort: + $ref: "#/components/links/UpdateAASViewByIdShort" + UpdateAASViewByIdShort: + $ref: "#/components/links/UpdateAASViewByIdShort" + DeleteAASViewByIdShort: + $ref: "#/components/links/DeleteAASViewByIdShort" "404": description: AssetAdministrationShell not found content: @@ -243,6 +250,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" + links: + ReadAASViewByIdShort: + $ref: "#/components/links/ReadAASViewByIdShort" + UpdateAASViewByIdShort: + $ref: "#/components/links/UpdateAASViewByIdShort" + DeleteAASViewByIdShort: + $ref: "#/components/links/DeleteAASViewByIdShort" "404": description: AssetAdministrationShell or View not found content: @@ -268,6 +282,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ViewResult" + links: + ReadAASViewByIdShort: + $ref: "#/components/links/ReadAASViewByIdShort" + UpdateAASViewByIdShort: + $ref: "#/components/links/UpdateAASViewByIdShort" + DeleteAASViewByIdShort: + $ref: "#/components/links/DeleteAASViewByIdShort" "201": description: Success (idShort changed) content: @@ -279,6 +300,11 @@ paths: description: The new URL of the View schema: type: string + links: + ReadAASViewByIdShort: + $ref: "#/components/links/ReadAASViewByIdShort" + DeleteAASViewByIdShort: + $ref: "#/components/links/DeleteAASViewByIdShort" "404": description: AssetAdministrationShell or View not found content: @@ -318,6 +344,22 @@ paths: tags: - Asset Administration Shell Interface components: + links: + ReadAASViewByIdShort: + description: The `idShort` of the returned View can be used to read the View. + operationId: ReadAASView + parameters: + view-idShort: "$response.body#/data/idShort" + UpdateAASViewByIdShort: + description: The `idShort` of the returned View can be used to update the View. + operationId: UpdateAASView + parameters: + view-idShort: "$response.body#/data/idShort" + DeleteAASViewByIdShort: + description: The `idShort` of the returned View can be used to delete the View. + operationId: DeleteAASView + parameters: + view-idShort: "$response.body#/data/idShort" schemas: BaseResult: type: object diff --git a/test/adapter/http-api-oas-submodel.yaml b/test/adapter/http-api-oas-submodel.yaml index fa1f43cb6..79ac905da 100644 --- a/test/adapter/http-api-oas-submodel.yaml +++ b/test/adapter/http-api-oas-submodel.yaml @@ -88,6 +88,13 @@ paths: description: The URL of the created Constraint schema: type: string + links: + ReadSubmodelQualifierByType: + $ref: "#/components/links/ReadSubmodelQualifierByType" + UpdateSubmodelQualifierByType: + $ref: "#/components/links/UpdateSubmodelQualifierByType" + DeleteSubmodelQualifierByType: + $ref: "#/components/links/DeleteSubmodelQualifierByType" "404": description: Submodel not found content: @@ -126,6 +133,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + links: + ReadSubmodelQualifierByType: + $ref: "#/components/links/ReadSubmodelQualifierByType" + UpdateSubmodelQualifierByType: + $ref: "#/components/links/UpdateSubmodelQualifierByType" + DeleteSubmodelQualifierByType: + $ref: "#/components/links/DeleteSubmodelQualifierByType" "404": description: Submodel or Constraint not found content: @@ -151,6 +165,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + links: + ReadSubmodelQualifierByType: + $ref: "#/components/links/ReadSubmodelQualifierByType" + UpdateSubmodelQualifierByType: + $ref: "#/components/links/UpdateSubmodelQualifierByType" + DeleteSubmodelQualifierByType: + $ref: "#/components/links/DeleteSubmodelQualifierByType" "201": description: Success (type changed) content: @@ -162,6 +183,13 @@ paths: description: The new URL of the Qualifier schema: type: string + links: + ReadSubmodelQualifierByType: + $ref: "#/components/links/ReadSubmodelQualifierByType" + UpdateSubmodelQualifierByType: + $ref: "#/components/links/UpdateSubmodelQualifierByType" + DeleteSubmodelQualifierByType: + $ref: "#/components/links/DeleteSubmodelQualifierByType" "404": description: Submodel or Constraint not found content: @@ -241,6 +269,13 @@ paths: description: The URL of the created SubmodelElement schema: type: string + links: + ReadSubmodelSubmodelElementByIdShortAfterPost: + $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPost" + UpdateSubmodelSubmodelElementByIdShortAfterPost: + $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPost" + DeleteSubmodelSubmodelElementByIdShortAfterPost: + $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPost" "404": description: Submodel not found content: @@ -425,6 +460,13 @@ paths: description: The URL of the created SubmodelElement schema: type: string + links: + ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPostWithPath" + UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath" + DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath" "400": description: Invalid idShort or SubmodelElement exists, but is not a SubmodelElementCollection, so /value is not possible content: @@ -505,6 +547,13 @@ paths: description: The URL of the created SubmodelElement schema: type: string + links: + ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPostWithPath" + UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath" + DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath" "400": description: Invalid idShort or SubmodelElement exists, but is not an AnnotatedRelationshipElement, so /annotation is not possible content: @@ -585,6 +634,13 @@ paths: description: The URL of the created SubmodelElement schema: type: string + links: + ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/ReadSubmodelSubmodelElementByIdShortAfterPostWithPath" + UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath" + DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: + $ref: "#/components/links/DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath" "400": description: Invalid idShort or SubmodelElement exists, but is not an Entity, so /statement is not possible content: @@ -665,6 +721,13 @@ paths: description: The URL of the created Constraint schema: type: string + links: + ReadSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" + UpdateSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" + DeleteSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" "400": description: Invalid idShort content: @@ -715,6 +778,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + links: + ReadSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" + UpdateSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" + DeleteSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" "400": description: Invalid idShort content: @@ -746,6 +816,13 @@ paths: "application/json": schema: $ref: "#/components/schemas/ConstraintResult" + links: + ReadSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" + UpdateSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" + DeleteSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" "201": description: Success (type changed) content: @@ -757,6 +834,13 @@ paths: description: The new URL of the Qualifier schema: type: string + links: + ReadSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/ReadSubmodelSubmodelElementQualifierByType" + UpdateSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/UpdateSubmodelSubmodelElementQualifierByType" + DeleteSubmodelSubmodelElementQualifierByType: + $ref: "#/components/links/DeleteSubmodelSubmodelElementQualifierByType" "400": description: Invalid idShort content: @@ -808,6 +892,70 @@ paths: tags: - Submodel Interface components: + links: + ReadSubmodelQualifierByType: + description: The `type` of the returned Qualifier can be used to read the Qualifier. + operationId: ReadSubmodelConstraint + parameters: + qualifier-type: "$response.body#/data/type" + UpdateSubmodelQualifierByType: + description: The `type` of the returned Qualifier can be used to update the Qualifier. + operationId: UpdateSubmodelConstraint + parameters: + qualifier-type: "$response.body#/data/type" + DeleteSubmodelQualifierByType: + description: The `type` of the returned Qualifier can be used to delete the Qualifier. + operationId: DeleteSubmodelConstraint + parameters: + qualifier-type: "$response.body#/data/type" + ReadSubmodelSubmodelElementByIdShortAfterPost: + description: The `idShort` of the returned SubmodelElement can be used to read the SubmodelElement. + operationId: ReadSubmodelSubmodelElement + parameters: + idShort-path: "!{$response.body#/data/idShort}" + UpdateSubmodelSubmodelElementByIdShortAfterPost: + description: The `idShort` of the returned SubmodelElement can be used to update the SubmodelElement. + operationId: UpdateSubmodelSubmodelElement + parameters: + idShort-path: "!{$response.body#/data/idShort}" + DeleteSubmodelSubmodelElementByIdShortAfterPost: + description: The `idShort` of the returned SubmodelElement can be used to delete the SubmodelElement. + operationId: DeleteSubmodelSubmodelElement + parameters: + idShort-path: "!{$response.body#/data/idShort}" + ReadSubmodelSubmodelElementByIdShortAfterPostWithPath: + description: The `idShort` of the returned SubmodelElement can be used to read the SubmodelElement. + operationId: ReadSubmodelSubmodelElement + parameters: + idShort-path: "{$request.path.idShort-path}/!{$response.body#/data/idShort}" + UpdateSubmodelSubmodelElementByIdShortAfterPostWithPath: + description: The `idShort` of the returned SubmodelElement can be used to update the SubmodelElement. + operationId: UpdateSubmodelSubmodelElement + parameters: + idShort-path: "{$request.path.idShort-path}/!{$response.body#/data/idShort}" + DeleteSubmodelSubmodelElementByIdShortAfterPostWithPath: + description: The `idShort` of the returned SubmodelElement can be used to delete the SubmodelElement. + operationId: DeleteSubmodelSubmodelElement + parameters: + idShort-path: "{$request.path.idShort-path}/!{$response.body#/data/idShort}" + ReadSubmodelSubmodelElementQualifierByType: + description: The `type` of the returned Qualifier can be used to read the Qualifier. + operationId: ReadSubmodelSubmodelElementConstraint + parameters: + idShort-path: "$request.path.idShort-path" + qualifier-type: "$response.body#/type" + UpdateSubmodelSubmodelElementQualifierByType: + description: The `type` of the returned Qualifier can be used to update the Qualifier. + operationId: UpdateSubmodelSubmodelElementConstraint + parameters: + idShort-path: "$request.path.idShort-path" + qualifier-type: "$response.body#/type" + DeleteSubmodelSubmodelElementQualifierByType: + description: The `type` of the returned Qualifier can be used to delete the Qualifier. + operationId: DeleteSubmodelSubmodelElementConstraint + parameters: + idShort-path: "$request.path.idShort-path" + qualifier-type: "$response.body#/type" schemas: BaseResult: type: object From faa8d450e3dab6d2b49b9b7ccc1629b998831931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Jul 2021 04:51:41 +0200 Subject: [PATCH 072/157] test: add http api tests requirements.txt: add hypothesis and schemathesis .gitignore: add /.hypothesis/ directory --- .gitignore | 1 + requirements.txt | 2 + test/adapter/test_http.py | 132 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 test/adapter/test_http.py diff --git a/.gitignore b/.gitignore index 397552b70..b0d86626a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ /.coverage /htmlcov/ /docs/build/ +/.hypothesis/ # customized config files /test/test_config.ini diff --git a/requirements.txt b/requirements.txt index f023f178d..9a5813f34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ types-python-dateutil pyecma376-2>=0.2.4 urllib3>=1.26,<2.0 Werkzeug>=1.0.1,<2 +schemathesis~=3.7 +hypothesis~=6.13 diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py new file mode 100644 index 000000000..d62c8a1ef --- /dev/null +++ b/test/adapter/test_http.py @@ -0,0 +1,132 @@ +# Copyright 2021 PyI40AAS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +""" +This test uses the schemathesis package to perform automated stateful testing on the implemented http api. Requests +are created automatically based on the json schemata given in the api specification, responses are also validated +against said schemata. + +For data generation schemathesis uses hypothesis and hypothesis-jsonschema, hence the name. hypothesis is a library +for automated, property-based testing. It can generate test cases based on strategies. hypothesis-jsonschema is such +a strategy for generating data that matches a given JSON schema. + +schemathesis allows stateful testing by generating a statemachine based on the OAS links contained in the specification. +This is applied here with the APIWorkflowAAS and APIWorkflowSubmodel classes. They inherit the respective state machine +and offer an automatically generated python unittest TestCase. +""" + +# TODO: lookup schemathesis deps and add them to the readme +# TODO: implement official Plattform I4.0 HTTP API +# TODO: check required properties of schema +# TODO: add id_short format to schemata + +import os +import pathlib +import schemathesis +import hypothesis.strategies +import random +import werkzeug.urls + +from aas import model +from aas.adapter.http import WSGIApp, identifier_uri_encode +from aas.examples.data.example_aas import create_full_example + +from typing import Set + + +def _encode_and_quote(identifier: model.Identifier) -> str: + return werkzeug.urls.url_quote(werkzeug.urls.url_quote(identifier_uri_encode(identifier), safe=""), safe="") + + +def _check_transformed(response, case): + """ + This helper function performs an additional checks on requests that have been *transformed*, i.e. requests, that + resulted from schemathesis using an OpenAPI Spec link. It asserts, that requests that are performed after a link has + been used, must be successful and result in a 2xx response. The exception are requests where hypothesis generates + invalid data (data, that validates against the schema, but is still semantically invalid). Such requests would + result in a 422 - Unprocessable Entity, which is why the 422 status code is ignored here. + """ + if case.source is not None: + assert 200 <= response.status_code < 300 or response.status_code == 422 + + +# define some settings for hypothesis, used in both api test cases +HYPOTHESIS_SETTINGS = hypothesis.settings( + max_examples=int(os.getenv("HYPOTHESIS_MAX_EXAMPLES", 10)), + stateful_step_count=5, + # disable the filter_too_much health check, which triggers if a strategy filters too much data, raising an error + suppress_health_check=[hypothesis.HealthCheck.filter_too_much], + # disable data generation deadlines, which would result in an error if data generation takes too much time + deadline=None +) + +BASE_URL = "/api/v1" +IDENTIFIER_AAS: Set[str] = set() +IDENTIFIER_SUBMODEL: Set[str] = set() + +# register hypothesis strategy for generating valid idShorts +ID_SHORT_STRATEGY = hypothesis.strategies.from_regex(r"\A[A-Za-z_][0-9A-Za-z_]*\Z") +schemathesis.register_string_format("id_short", ID_SHORT_STRATEGY) + +# store identifiers of available AAS and Submodels +for obj in create_full_example(): + if isinstance(obj, model.AssetAdministrationShell): + IDENTIFIER_AAS.add(_encode_and_quote(obj.identification)) + if isinstance(obj, model.Submodel): + IDENTIFIER_SUBMODEL.add(_encode_and_quote(obj.identification)) + +# load aas and submodel api specs +AAS_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-aas.yaml", + app=WSGIApp(create_full_example())) + +SUBMODEL_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-submodel.yaml", + app=WSGIApp(create_full_example())) + + +class APIWorkflowAAS(AAS_SCHEMA.as_state_machine()): # type: ignore + def setup(self): + self.schema.app.object_store = create_full_example() + # select random identifier for each test scenario + self.schema.base_url = BASE_URL + "/aas/" + random.choice(tuple(IDENTIFIER_AAS)) + + def transform(self, result, direction, case): + out = super().transform(result, direction, case) + print("transformed") + print(out) + print(result.response, direction.name) + return out + + def validate_response(self, response, case, additional_checks=()) -> None: + super().validate_response(response, case, additional_checks + (_check_transformed,)) + + +class APIWorkflowSubmodel(SUBMODEL_SCHEMA.as_state_machine()): # type: ignore + def setup(self): + self.schema.app.object_store = create_full_example() + self.schema.base_url = BASE_URL + "/submodels/" + random.choice(tuple(IDENTIFIER_SUBMODEL)) + + def transform(self, result, direction, case): + out = super().transform(result, direction, case) + print("transformed") + print(out) + print(result.response, direction.name) + return out + + def validate_response(self, response, case, additional_checks=()) -> None: + super().validate_response(response, case, additional_checks + (_check_transformed,)) + + +# APIWorkflow.TestCase is a standard python unittest.TestCase +ApiTestAAS = APIWorkflowAAS.TestCase +ApiTestAAS.settings = HYPOTHESIS_SETTINGS + +ApiTestSubmodel = APIWorkflowSubmodel.TestCase +ApiTestSubmodel.settings = HYPOTHESIS_SETTINGS From dcae764f0c9060dac2af4342409d2d5a3fbeeee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Jul 2021 14:28:31 +0200 Subject: [PATCH 073/157] adapter.http: fix NamespaceSet containment checks and removals ... in accordance to 0e343ac0810dead528e6dd43ef3e7752d0387c5f. --- basyx/aas/adapter/http.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 95f768b71..b02f4b80c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -558,7 +558,7 @@ def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapt aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() view = parse_request_body(request, model.View) - if ("id_short", view.id_short) in aas.view: + if aas.view.contains_id("id_short", view.id_short): raise Conflict(f"View with id_short {view.id_short} already exists!") aas.view.add(view) aas.commit() @@ -604,9 +604,9 @@ def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() view_idshort = url_args["view_idshort"] - if ("id_short", view_idshort) not in aas.view: + if not aas.view.contains_id("id_short", view_idshort): raise NotFound(f"No view with id_short {view_idshort} found!") - aas.view.remove(("id_short", view_idshort)) + aas.view.remove_by_id("id_short", view_idshort) return response_t(Result(None)) # --------- SUBMODEL ROUTES --------- @@ -630,7 +630,7 @@ def post_submodel_submodel_elements(self, request: Request, url_args: Dict, map_ # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - if ("id_short", submodel_element.id_short) in submodel.submodel_element: + if submodel.submodel_element.contains_id("id_short", submodel_element.id_short): raise Conflict(f"Submodel element with id_short {submodel_element.id_short} already exists!") submodel.submodel_element.add(submodel_element) submodel.commit() @@ -711,7 +711,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier = parse_request_body(request, model.Qualifier) - if ("type", qualifier.type) in sm_or_se.qualifier: + if sm_or_se.qualifier.contains_id("type", qualifier.type): raise Conflict(f"Qualifier with type {qualifier.type} already exists!") sm_or_se.qualifier.add(qualifier) sm_or_se.commit() @@ -740,7 +740,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: raise UnprocessableEntity(f"Type of new qualifier {new_qualifier} doesn't not match " f"the current submodel element {qualifier}") qualifier_type_changed = qualifier_type != new_qualifier.type - if qualifier_type_changed and ("type", new_qualifier.type) in sm_or_se.qualifier: + if qualifier_type_changed and sm_or_se.qualifier.contains_id("type", new_qualifier.type): raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") sm_or_se.remove_qualifier_by_type(qualifier.type) sm_or_se.qualifier.add(new_qualifier) @@ -797,7 +797,7 @@ def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response if not isinstance(submodel_element, type_): raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") new_submodel_element = parse_request_body(request, request_body_type) - if ("id_short", new_submodel_element.id_short) in getattr(submodel_element, attr): + if getattr(submodel_element, attr).contains_id("id_short", new_submodel_element.id_short): raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") getattr(submodel_element, attr).add(new_submodel_element) submodel_element.commit() From 1cb4d2e5c80d222b03b1ff39c07e4daeb727af49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 27 Jul 2021 16:48:25 +0200 Subject: [PATCH 074/157] adapter.http: update AAS interface for new api spec update response encoders update response data --- basyx/aas/adapter/http.py | 311 +++++++++++++++++--------------------- 1 file changed, 135 insertions(+), 176 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index b02f4b80c..9c6a0ce95 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -11,6 +11,7 @@ import abc +import datetime import enum import io import json @@ -24,83 +25,89 @@ from aas import model from .xml import XMLConstructables, read_aas_xml_element, xml_serialization -from .json import StrippedAASToJsonEncoder, StrictStrippedAASFromJsonDecoder +from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union +from typing import Callable, Dict, List, Optional, Type, TypeVar, Union + + +# TODO: support the path/reference/etc. parameter @enum.unique -class ErrorType(enum.Enum): - UNSPECIFIED = enum.auto() - DEBUG = enum.auto() - INFORMATION = enum.auto() +class MessageType(enum.Enum): + UNDEFINED = enum.auto() + INFO = enum.auto() WARNING = enum.auto() ERROR = enum.auto() - FATAL = enum.auto() EXCEPTION = enum.auto() def __str__(self): return self.name.capitalize() -class Error: - def __init__(self, code: str, text: str, type_: ErrorType = ErrorType.UNSPECIFIED): - self.type = type_ +class Message: + def __init__(self, code: str, text: str, type_: MessageType = MessageType.UNDEFINED, + timestamp: Optional[datetime.datetime] = None): self.code = code self.text = text - - -ResultData = Union[object, Tuple[object, ...]] + self.messageType = type_ + self.timestamp = timestamp if timestamp is not None else datetime.datetime.utcnow() class Result: - def __init__(self, data: Optional[Union[ResultData, Error]]): - # the following is True when data is None, which is the expected behavior - self.success: bool = not isinstance(data, Error) - self.data: Optional[ResultData] = None - self.error: Optional[Error] = None - if isinstance(data, Error): - self.error = data - else: - self.data = data + def __init__(self, success: bool, messages: Optional[List[Message]] = None): + if messages is None: + messages = [] + self.success: bool = success + self.messages: List[Message] = messages -class ResultToJsonEncoder(StrippedAASToJsonEncoder): +class ResultToJsonEncoder(AASToJsonEncoder): @classmethod def _result_to_json(cls, result: Result) -> Dict[str, object]: return { "success": result.success, - "error": result.error, - "data": result.data + "messages": result.messages } @classmethod - def _error_to_json(cls, error: Error) -> Dict[str, object]: + def _message_to_json(cls, message: Message) -> Dict[str, object]: return { - "type": error.type, - "code": error.code, - "text": error.text + "messageType": message.messageType, + "text": message.text, + "code": message.code, + "timestamp": message.timestamp.isoformat() } def default(self, obj: object) -> object: if isinstance(obj, Result): return self._result_to_json(obj) - if isinstance(obj, Error): - return self._error_to_json(obj) - if isinstance(obj, ErrorType): + if isinstance(obj, Message): + return self._message_to_json(obj) + if isinstance(obj, MessageType): return str(obj) return super().default(obj) +class StrippedResultToJsonEncoder(ResultToJsonEncoder): + stripped = True + + +ResponseData = Union[Result, object, List[object]] + + class APIResponse(abc.ABC, Response): @abc.abstractmethod - def __init__(self, result: Result, *args, **kwargs): + def __init__(self, obj: Optional[ResponseData] = None, stripped: bool = False, *args, **kwargs): super().__init__(*args, **kwargs) - self.data = self.serialize(result) + if obj is None: + self.status_code = 204 + else: + self.data = self.serialize(obj, stripped) @abc.abstractmethod - def serialize(self, result: Result) -> str: + def serialize(self, obj: ResponseData, stripped: bool) -> str: pass @@ -108,18 +115,33 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, result: Result) -> str: - return json.dumps(result, cls=ResultToJsonEncoder, separators=(",", ":")) + def serialize(self, obj: ResponseData, stripped: bool) -> str: + return json.dumps(obj, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, + separators=(",", ":")) class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, result: Result) -> str: - result_elem = result_to_xml(result, nsmap=xml_serialization.NS_MAP) - etree.cleanup_namespaces(result_elem) - return etree.tostring(result_elem, xml_declaration=True, encoding="utf-8") + def serialize(self, obj: ResponseData, stripped: bool) -> str: + # TODO: xml serialization doesn't support stripped objects + if isinstance(obj, Result): + response_elem = result_to_xml(obj, nsmap=xml_serialization.NS_MAP) + etree.cleanup_namespaces(response_elem) + else: + if isinstance(obj, list): + response_elem = etree.Element("list", nsmap=xml_serialization.NS_MAP) + for obj in obj: + response_elem.append(aas_object_to_xml(obj)) + etree.cleanup_namespaces(response_elem) + else: + # dirty hack to be able to use the namespace prefixes defined in xml_serialization.NS_MAP + parent = etree.Element("parent", nsmap=xml_serialization.NS_MAP) + response_elem = aas_object_to_xml(obj) + parent.append(response_elem) + etree.cleanup_namespaces(parent) + return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") class XmlResponseAlt(XmlResponse): @@ -131,51 +153,41 @@ def result_to_xml(result: Result, **kwargs) -> etree.Element: result_elem = etree.Element("result", **kwargs) success_elem = etree.Element("success") success_elem.text = xml_serialization.boolean_to_xml(result.success) - if result.error is None: - error_elem = etree.Element("error") - else: - error_elem = error_to_xml(result.error) - data_elem = etree.Element("data") - if result.data is not None: - for element in result_data_to_xml(result.data): - data_elem.append(element) + messages_elem = etree.Element("messages") + for message in result.messages: + messages_elem.append(message_to_xml(message)) + result_elem.append(success_elem) - result_elem.append(error_elem) - result_elem.append(data_elem) + result_elem.append(messages_elem) return result_elem -def error_to_xml(error: Error) -> etree.Element: - error_elem = etree.Element("error") - type_elem = etree.Element("type") - type_elem.text = str(error.type) - code_elem = etree.Element("code") - code_elem.text = error.code +def message_to_xml(message: Message) -> etree.Element: + message_elem = etree.Element("message") + message_type_elem = etree.Element("messageType") + message_type_elem.text = str(message.messageType) text_elem = etree.Element("text") - text_elem.text = error.text - error_elem.append(type_elem) - error_elem.append(code_elem) - error_elem.append(text_elem) - return error_elem - + text_elem.text = message.text + code_elem = etree.Element("code") + code_elem.text = message.code + timestamp_elem = etree.Element("timestamp") + timestamp_elem.text = message.timestamp.isoformat() -def result_data_to_xml(data: ResultData) -> Iterable[etree.Element]: - # for xml we can just append multiple elements to the data element - # so multiple elements will be handled the same as a single element - if not isinstance(data, tuple): - data = (data,) - for obj in data: - yield aas_object_to_xml(obj) + message_elem.append(message_type_elem) + message_elem.append(text_elem) + message_elem.append(code_elem) + message_elem.append(timestamp_elem) + return message_elem def aas_object_to_xml(obj: object) -> etree.Element: # TODO: a similar function should be implemented in the xml serialization if isinstance(obj, model.AssetAdministrationShell): return xml_serialization.asset_administration_shell_to_xml(obj) + if isinstance(obj, model.AssetInformation): + return xml_serialization.asset_information_to_xml(obj) if isinstance(obj, model.Reference): return xml_serialization.reference_to_xml(obj) - if isinstance(obj, model.View): - return xml_serialization.view_to_xml(obj) if isinstance(obj, model.Submodel): return xml_serialization.submodel_to_xml(obj) # TODO: xml serialization needs a constraint_to_xml() function @@ -208,14 +220,18 @@ def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, res if location is not None: headers.append(("Location", location)) if exception.code and exception.code >= 400: - error = Error(type(exception).__name__, exception.description if exception.description is not None else "", - ErrorType.ERROR) - result = Result(error) + message = Message(type(exception).__name__, exception.description if exception.description is not None else "", + MessageType.ERROR) + result = Result(False, [message]) else: - result = Result(None) + result = Result(False) return response_type(result, status=exception.code, headers=headers) +def is_stripped_request(request: Request) -> bool: + return request.args.get("level") == "core" + + T = TypeVar("T") @@ -228,6 +244,8 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: which should limit the maximum content length. """ type_constructables_map = { + model.AssetAdministrationShell: XMLConstructables.ASSET_ADMINISTRATION_SHELL, + model.AssetInformation: XMLConstructables.ASSET_INFORMATION, model.AASReference: XMLConstructables.AAS_REFERENCE, model.View: XMLConstructables.VIEW, model.Qualifier: XMLConstructables.QUALIFIER, @@ -245,17 +263,22 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: try: if request.mimetype == "application/json": - rv = json.loads(request.get_data(), cls=StrictStrippedAASFromJsonDecoder) + decoder: Type[StrictAASFromJsonDecoder] = StrictStrippedAASFromJsonDecoder if is_stripped_request(request) \ + else StrictAASFromJsonDecoder + rv = json.loads(request.get_data(), cls=decoder) # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema # TODO: json deserialization will always create an AASReference[Submodel], xml deserialization determines # that automatically if expect_type is model.AASReference: - rv = StrictStrippedAASFromJsonDecoder._construct_aas_reference(rv, model.Submodel) + rv = decoder._construct_aas_reference(rv, model.Submodel) + elif expect_type is model.AssetInformation: + rv = decoder._construct_asset_information(rv, model.AssetInformation) else: try: xml_data = io.BytesIO(request.get_data()) - rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], stripped=True, failsafe=False) + rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], + stripped=is_stripped_request(request), failsafe=False) except (KeyError, ValueError) as e: # xml deserialization creates an error chain. since we only return one error, return the root cause f: BaseException = e @@ -331,25 +354,17 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - Submount("/aas/", [ + Submount("/aas//aas", [ Rule("/", methods=["GET"], endpoint=self.get_aas), + Rule("/", methods=["PUT"], endpoint=self.put_aas), + Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/assetInformation", methods=["PUT"], endpoint=self.put_aas_asset_information), Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["GET"], - endpoint=self.get_aas_submodel_refs_specific), + Rule("//", methods=["PUT"], + endpoint=self.put_aas_submodel_refs), Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) - ]), - Submount("/views", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_views), - Rule("/", methods=["POST"], endpoint=self.post_aas_views), - Rule("//", methods=["GET"], - endpoint=self.get_aas_views_specific), - Rule("//", methods=["PUT"], - endpoint=self.put_aas_views_specific), - Rule("//", methods=["DELETE"], - endpoint=self.delete_aas_views_specific) ]) ]), Submount("/submodels/", [ @@ -497,117 +512,61 @@ def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - return response_t(Result(aas)) + return response_t(aas, stripped=is_stripped_request(request)) - def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - return response_t(Result(tuple(aas.submodel))) - - def post_aas_submodel_refs(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) - aas_identifier = url_args["aas_id"] - aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) - aas.update() - sm_ref = parse_request_body(request, model.AASReference) - # to give a location header in the response we have to be able to get the submodel identifier from the reference - try: - submodel_identifier = sm_ref.get_identifier() - except ValueError as e: - raise UnprocessableEntity(f"Can't resolve submodel identifier for given reference!") from e - if sm_ref in aas.submodel: - raise Conflict(f"{sm_ref!r} already exists!") - aas.submodel.add(sm_ref) + aas_new = parse_request_body(request, model.AssetAdministrationShell) + aas.update_from(aas_new) aas.commit() - created_resource_url = map_adapter.build(self.get_aas_submodel_refs_specific, { - "aas_id": aas_identifier, - "sm_id": submodel_identifier - }, force_external=True) - return response_t(Result(sm_ref), status=201, headers={"Location": created_resource_url}) + return response_t() - def get_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) - return response_t(Result(sm_ref)) + return response_t(aas.asset_information) - def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) - # use remove(sm_ref) because it raises a KeyError if sm_ref is not present - # sm_ref must be present because _get_aas_submodel_reference_by_submodel_identifier() found it there - # so if sm_ref is not in aas.submodel, this implementation is bugged and the raised KeyError will result - # in an InternalServerError - aas.submodel.remove(sm_ref) + aas.asset_information = parse_request_body(request, model.AssetInformation) aas.commit() - return response_t(Result(None)) + return response_t() - def get_aas_views(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - return response_t(Result(tuple(aas.view))) + return response_t(list(aas.submodel)) - def post_aas_views(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + def put_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - view = parse_request_body(request, model.View) - if aas.view.contains_id("id_short", view.id_short): - raise Conflict(f"View with id_short {view.id_short} already exists!") - aas.view.add(view) + sm_ref = parse_request_body(request, model.AASReference) + if sm_ref in aas.submodel: + raise Conflict(f"{sm_ref!r} already exists!") + aas.submodel.add(sm_ref) aas.commit() - created_resource_url = map_adapter.build(self.get_aas_views_specific, { - "aas_id": aas_identifier, - "view_idshort": view.id_short - }, force_external=True) - return response_t(Result(view), status=201, headers={"Location": created_resource_url}) - - def get_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() - view_idshort = url_args["view_idshort"] - view = aas.view.get("id_short", view_idshort) - if view is None: - raise NotFound(f"No view with id_short {view_idshort} found!") - return response_t(Result(view)) + return response_t(sm_ref, status=201) - def put_aas_views_specific(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) - aas_identifier = url_args["aas_id"] - aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) - aas.update() - view_idshort = url_args["view_idshort"] - view = aas.view.get("id_short", view_idshort) - if view is None: - raise NotFound(f"No view with id_short {view_idshort} found!") - new_view = parse_request_body(request, model.View) - # TODO: raise conflict if the following fails - view.update_from(new_view) - view.commit() - if view_idshort.upper() != view.id_short.upper(): - created_resource_url = map_adapter.build(self.put_aas_views_specific, { - "aas_id": aas_identifier, - "view_idshort": view.id_short - }, force_external=True) - return response_t(Result(view), status=201, headers={"Location": created_resource_url}) - return response_t(Result(view)) - - def delete_aas_views_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - view_idshort = url_args["view_idshort"] - if not aas.view.contains_id("id_short", view_idshort): - raise NotFound(f"No view with id_short {view_idshort} found!") - aas.view.remove_by_id("id_short", view_idshort) - return response_t(Result(None)) + sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) + # use remove(sm_ref) because it raises a KeyError if sm_ref is not present + # sm_ref must be present because _get_aas_submodel_reference_by_submodel_identifier() found it there + # so if sm_ref is not in aas.submodel, this implementation is bugged and the raised KeyError will result + # in an InternalServerError + aas.submodel.remove(sm_ref) + aas.commit() + return response_t() # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: From 7ceef25ed2baad0f266e98441e92e05febd7bcc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 28 Jul 2021 03:15:24 +0200 Subject: [PATCH 075/157] adapter.http: update id_short_path and Identifier converters --- basyx/aas/adapter/http.py | 68 ++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 9c6a0ce95..935b06533 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -11,6 +11,8 @@ import abc +import base64 +import binascii import datetime import enum import io @@ -294,58 +296,50 @@ def parse_request_body(request: Request, expect_type: Type[T]) -> T: return rv -def identifier_uri_encode(id_: model.Identifier) -> str: - return IDENTIFIER_TYPES[id_.id_type] + ":" + werkzeug.urls.url_quote(id_.id, safe="") - - -def identifier_uri_decode(id_str: str) -> model.Identifier: - try: - id_type_str, id_ = id_str.split(":", 1) - except ValueError: - raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") - id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) - if id_type is None: - raise ValueError(f"IdentifierType '{id_type_str}' is invalid") - return model.Identifier(werkzeug.urls.url_unquote(id_), id_type) - - class IdentifierConverter(werkzeug.routing.UnicodeConverter): + encoding = "utf-8" + def to_url(self, value: model.Identifier) -> str: - return super().to_url(identifier_uri_encode(value)) + return super().to_url(base64.urlsafe_b64encode((IDENTIFIER_TYPES[value.id_type] + ":" + value.id) + .encode(self.encoding))) def to_python(self, value: str) -> model.Identifier: + value = super().to_python(value) try: - return identifier_uri_decode(super().to_python(value)) - except ValueError as e: - raise BadRequest(str(e)) from e - + decoded = base64.urlsafe_b64decode(super().to_python(value)).decode(self.encoding) + except binascii.Error: + raise BadRequest(f"Encoded identifier {value} is invalid base64url!") + except UnicodeDecodeError: + raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + id_type_str, id_ = decoded.split(":", 1) + try: + return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type_str]) + except KeyError: + raise BadRequest(f"{id_type_str} is not a valid identifier type!") -def validate_id_short(id_short: str) -> bool: - try: - model.MultiLanguageProperty(id_short) - except model.AASConstraintViolation: - return False - return True +class IdShortPathConverter(werkzeug.routing.UnicodeConverter): + id_short_sep = "." -class IdShortPathConverter(werkzeug.routing.PathConverter): - id_short_prefix = "!" + @classmethod + def validate_id_short(cls, id_short: str) -> bool: + try: + model.MultiLanguageProperty(id_short) + except model.AASConstraintViolation: + return False + return True def to_url(self, value: List[str]) -> str: for id_short in value: - if not validate_id_short(id_short): + if not self.validate_id_short(id_short): raise ValueError(f"{id_short} is not a valid id_short!") - return "/".join([self.id_short_prefix + id_short for id_short in value]) + return super().to_url(".".join(id_short for id_short in value)) def to_python(self, value: str) -> List[str]: - id_shorts = super().to_python(value).split("/") - for idx, id_short in enumerate(id_shorts): - if not id_short.startswith(self.id_short_prefix): - raise werkzeug.routing.ValidationError - id_short = id_short[1:] - if not validate_id_short(id_short): + id_shorts = super().to_python(value).split(self.id_short_sep) + for id_short in id_shorts: + if not self.validate_id_short(id_short): raise BadRequest(f"{id_short} is not a valid id_short!") - id_shorts[idx] = id_short return id_shorts From d05be61cea96c614a751c0c2c05f8a295b0b935d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 28 Jul 2021 03:16:26 +0200 Subject: [PATCH 076/157] adapter.http: update submodel interface --- basyx/aas/adapter/http.py | 170 ++++++++++++++++++-------------------- 1 file changed, 80 insertions(+), 90 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 935b06533..42ed80ddf 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -348,7 +348,7 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - Submount("/aas//aas", [ + Submount("/shells//aas", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), @@ -361,38 +361,50 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.delete_aas_submodel_refs_specific) ]) ]), - Submount("/submodels/", [ + Submount("/submodels//submodel", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/submodelElements/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/submodelElements/", methods=["POST"], endpoint=self.post_submodel_submodel_elements), - Submount("/", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_specific_nested), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_specific_nested), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_specific_nested), - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - Rule("/values/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("/values/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("/annotations/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation")), - Rule("/annotations/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation", - request_body_type=model.DataElement)), # type: ignore - Rule("/statements/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, - "statement")), - Rule("/statements/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, - "statement")), + Rule("/", methods=["PUT"], endpoint=self.put_submodel), + Submount("/submodelElements", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Submount("/", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_id_short_path), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_id_short_path), + # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + Rule("/values/", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("/values/", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.SubmodelElementCollection, "value")), # type: ignore + Rule("/annotations/", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation")), + Rule("/annotations/", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr( + model.AnnotatedRelationshipElement, "annotation", + request_body_type=model.DataElement)), # type: ignore + Rule("/statements/", methods=["GET"], + endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, + "statement")), + Rule("/statements/", methods=["POST"], + endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, + "statement")), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) + ]), Submount("/constraints", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), @@ -403,16 +415,6 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("//", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_constraints), ]) - ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), ]) ]) ]) @@ -512,8 +514,7 @@ def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - aas_new = parse_request_body(request, model.AssetAdministrationShell) - aas.update_from(aas_new) + aas.update_from(parse_request_body(request, model.AssetAdministrationShell)) aas.commit() return response_t() @@ -564,82 +565,71 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - return response_t(Result(submodel)) + return response_t(submodel, stripped=is_stripped_request(request)) - def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - return response_t(Result(tuple(submodel.submodel_element))) + submodel.update_from(parse_request_body(request, model.Submodel)) + submodel.commit() + return response_t() - def post_submodel_submodel_elements(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent, semanticId, parentPath parameters response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - if submodel.submodel_element.contains_id("id_short", submodel_element.id_short): - raise Conflict(f"Submodel element with id_short {submodel_element.id_short} already exists!") - submodel.submodel_element.add(submodel_element) - submodel.commit() - created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_specific_nested, { - "submodel_id": submodel_identifier, - "id_shorts": [submodel_element.id_short] - }, force_external=True) - return response_t(Result(submodel_element), status=201, headers={"Location": created_resource_url}) + return response_t(list(submodel.submodel_element)) - def get_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) - return response_t(Result(submodel_element)) + return response_t(submodel_element, stripped=is_stripped_request(request)) - def put_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, - map_adapter: MapAdapter) -> Response: + def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content parameter response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] submodel = self._get_obj_ts(submodel_identifier, model.Submodel) submodel.update() id_short_path = url_args["id_shorts"] - submodel_element = self._get_nested_submodel_element(submodel, id_short_path) - current_id_short = submodel_element.id_short - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - new_submodel_element = parse_request_body(request, model.SubmodelElement) # type: ignore - if type(submodel_element) is not type(new_submodel_element): - raise UnprocessableEntity(f"Type of new submodel element {new_submodel_element} doesn't not match " - f"the current submodel element {submodel_element}") - # TODO: raise conflict if the following fails - submodel_element.update_from(new_submodel_element) + parent = self._expect_namespace( + self._get_nested_submodel_element(submodel, url_args["id_shorts"][:-1]), + id_short_path[-1] + ) + try: + submodel_element = parent.get_referable(id_short_path[-1]) + except KeyError: + # TODO: add new submodel element here, currently impossible + raise NotImplementedError("Adding submodel elements is currently unsupported!") + # return response_t(new_submodel_element, status=201) + # TODO: what if only data elements are allowed as children? + submodel_element.update_from(parse_request_body(request, model.SubmodelElement)) submodel_element.commit() - if new_submodel_element.id_short.upper() != current_id_short.upper(): - created_resource_url = map_adapter.build(self.put_submodel_submodel_elements_specific_nested, { - "submodel_id": submodel_identifier, - "id_shorts": id_short_path[:-1] + [submodel_element.id_short] - }, force_external=True) - return response_t(Result(submodel_element), status=201, headers={"Location": created_resource_url}) - return response_t(Result(submodel_element)) + return response_t() - def delete_submodel_submodel_elements_specific_nested(self, request: Request, url_args: Dict, **_kwargs) \ + def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - id_shorts: List[str] = url_args["id_shorts"] + id_short_path: List[str] = url_args["id_shorts"] parent: model.UniqueIdShortNamespace = submodel - if len(id_shorts) > 1: + if len(id_short_path) > 1: parent = self._expect_namespace( - self._get_nested_submodel_element(submodel, id_shorts[:-1]), - id_shorts[-1] + self._get_nested_submodel_element(submodel, id_short_path[:-1]), + id_short_path[-1] ) - self._namespace_submodel_element_op(parent, parent.remove_referable, id_shorts[-1]) - return response_t(Result(None)) + self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) + return response_t() def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: From 428a2bae40e08655c81dfe8e10e19278dfe37979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:13:16 +0200 Subject: [PATCH 077/157] adapter.http: remove hardcoded dot as IdShortPath separator --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 42ed80ddf..1431edeed 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -333,7 +333,7 @@ def to_url(self, value: List[str]) -> str: for id_short in value: if not self.validate_id_short(id_short): raise ValueError(f"{id_short} is not a valid id_short!") - return super().to_url(".".join(id_short for id_short in value)) + return super().to_url(self.id_short_sep.join(id_short for id_short in value)) def to_python(self, value: str) -> List[str]: id_shorts = super().to_python(value).split(self.id_short_sep) From 7daee1830715e4d8b7b5b6f326c4b2e6993b6a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:40:52 +0200 Subject: [PATCH 078/157] adapter.http: allow adding submodel elements --- basyx/aas/adapter/http.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 1431edeed..453c2dc5b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -605,14 +605,16 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg self._get_nested_submodel_element(submodel, url_args["id_shorts"][:-1]), id_short_path[-1] ) + # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement) # type: ignore try: submodel_element = parent.get_referable(id_short_path[-1]) except KeyError: - # TODO: add new submodel element here, currently impossible - raise NotImplementedError("Adding submodel elements is currently unsupported!") - # return response_t(new_submodel_element, status=201) - # TODO: what if only data elements are allowed as children? - submodel_element.update_from(parse_request_body(request, model.SubmodelElement)) + parent.add_referable(new_submodel_element) + new_submodel_element.commit() + return response_t(new_submodel_element, status=201) + submodel_element.update_from(new_submodel_element) submodel_element.commit() return response_t() From 758537b907bc1245b892d3e1623b1c05aac923f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:41:49 +0200 Subject: [PATCH 079/157] adapter.http: adjust submodel routes for new response type --- basyx/aas/adapter/http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 453c2dc5b..591bb4c67 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -641,9 +641,9 @@ def get_submodel_submodel_element_constraints(self, request: Request, url_args: sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: - return response_t(Result(tuple(sm_or_se.qualifier))) + return response_t(list(sm_or_se.qualifier)) try: - return response_t(Result(sm_or_se.get_qualifier_by_type(qualifier_type))) + return response_t(sm_or_se.get_qualifier_by_type(qualifier_type)) except KeyError: raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") @@ -665,7 +665,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: "id_shorts": id_shorts if len(id_shorts) != 0 else None, "qualifier_type": qualifier.type }, force_external=True) - return response_t(Result(qualifier), status=201, headers={"Location": created_resource_url}) + return response_t(qualifier, status=201, headers={"Location": created_resource_url}) def put_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ -> Response: @@ -696,8 +696,8 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: "id_shorts": id_shorts if len(id_shorts) != 0 else None, "qualifier_type": new_qualifier.type }, force_external=True) - return response_t(Result(new_qualifier), status=201, headers={"Location": created_resource_url}) - return response_t(Result(new_qualifier)) + return response_t(new_qualifier, status=201, headers={"Location": created_resource_url}) + return response_t(new_qualifier) def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: From ed906713b87fd24c8dc98e952e46aa67f7c14bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:19:33 +0200 Subject: [PATCH 080/157] adapter.http: use example without missing attributes (contains no views) because the view routes have been removed --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 591bb4c67..d30c553c8 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -757,5 +757,5 @@ def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response if __name__ == "__main__": from werkzeug.serving import run_simple # use example_aas_missing_attributes, because the AAS from example_aas has no views - from aas.examples.data.example_aas_missing_attributes import create_full_example + from aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From f4741fda7a0eadc2169897f91be705391a9c14f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 7 Sep 2021 19:29:35 +0200 Subject: [PATCH 081/157] adapter.http: make json/xml decoder interface independent of the request body add aas/submodel repository interface --- basyx/aas/adapter/http.py | 349 +++++++++++++++++++++----------------- 1 file changed, 197 insertions(+), 152 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d30c553c8..d908fd251 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -30,7 +30,7 @@ from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE -from typing import Callable, Dict, List, Optional, Type, TypeVar, Union +from typing import Callable, Dict, Iterator, List, Optional, Type, TypeVar, Union # TODO: support the path/reference/etc. parameter @@ -237,63 +237,105 @@ def is_stripped_request(request: Request) -> bool: T = TypeVar("T") -def parse_request_body(request: Request, expect_type: Type[T]) -> T: - """ - TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent - running out of memory. but it doesn't state how to check the content length - also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json schema - In the meeting (25.11.2020) we discussed, this may refer to a reverse proxy in front of this WSGI app, - which should limit the maximum content length. - """ +class HTTPApiDecoder: + # these are the types we can construct (well, only the ones we need) type_constructables_map = { model.AssetAdministrationShell: XMLConstructables.ASSET_ADMINISTRATION_SHELL, model.AssetInformation: XMLConstructables.ASSET_INFORMATION, model.AASReference: XMLConstructables.AAS_REFERENCE, - model.View: XMLConstructables.VIEW, + model.IdentifierKeyValuePair: XMLConstructables.IDENTIFIER_KEY_VALUE_PAIR, model.Qualifier: XMLConstructables.QUALIFIER, model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT } - if expect_type not in type_constructables_map: - raise TypeError(f"Parsing {expect_type} is not supported!") - - valid_content_types = ("application/json", "application/xml", "text/xml") + @classmethod + def check_type_supportance(cls, type_: type): + if type_ not in cls.type_constructables_map: + raise TypeError(f"Parsing {type_} is not supported!") - if request.mimetype not in valid_content_types: - raise werkzeug.exceptions.UnsupportedMediaType(f"Invalid content-type: {request.mimetype}! Supported types: " - + ", ".join(valid_content_types)) + @classmethod + def assert_type(cls, obj: object, type_: Type[T]) -> T: + if not isinstance(obj, type_): + raise UnprocessableEntity(f"Object {obj!r} is not of type {type_.__name__}!") + return obj - try: - if request.mimetype == "application/json": - decoder: Type[StrictAASFromJsonDecoder] = StrictStrippedAASFromJsonDecoder if is_stripped_request(request) \ - else StrictAASFromJsonDecoder - rv = json.loads(request.get_data(), cls=decoder) + @classmethod + def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool) -> List[T]: + cls.check_type_supportance(expect_type) + decoder: Type[StrictAASFromJsonDecoder] = StrictStrippedAASFromJsonDecoder if stripped \ + else StrictAASFromJsonDecoder + try: + parsed = json.loads(data, cls=decoder) + if not isinstance(parsed, list): + if not expect_single: + raise UnprocessableEntity(f"Expected List[{expect_type.__name__}], got {parsed!r}!") + parsed = [parsed] + elif expect_single: + raise UnprocessableEntity(f"Expected a single object of type {expect_type.__name__}, got {parsed!r}!") # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema # TODO: json deserialization will always create an AASReference[Submodel], xml deserialization determines # that automatically + constructor: Optional[Callable[..., T]] = None + args = [] if expect_type is model.AASReference: - rv = decoder._construct_aas_reference(rv, model.Submodel) + constructor = decoder._construct_aas_reference # type: ignore + args.append(model.Submodel) elif expect_type is model.AssetInformation: - rv = decoder._construct_asset_information(rv, model.AssetInformation) - else: - try: - xml_data = io.BytesIO(request.get_data()) - rv = read_aas_xml_element(xml_data, type_constructables_map[expect_type], - stripped=is_stripped_request(request), failsafe=False) - except (KeyError, ValueError) as e: - # xml deserialization creates an error chain. since we only return one error, return the root cause - f: BaseException = e - while f.__cause__ is not None: - f = f.__cause__ - raise f from e - except (KeyError, ValueError, TypeError, json.JSONDecodeError, etree.XMLSyntaxError, model.AASConstraintViolation) \ - as e: - raise UnprocessableEntity(str(e)) from e - - if not isinstance(rv, expect_type): - raise UnprocessableEntity(f"Object {rv!r} is not of type {expect_type.__name__}!") - return rv + constructor = decoder._construct_asset_information # type: ignore + elif expect_type is model.IdentifierKeyValuePair: + constructor = decoder._construct_identifier_key_value_pair # type: ignore + + if constructor is not None: + # construct elements that aren't self-identified + return [constructor(obj, *args) for obj in parsed] + + except (KeyError, ValueError, TypeError, json.JSONDecodeError, model.AASConstraintViolation) as e: + raise UnprocessableEntity(str(e)) from e + + return [cls.assert_type(obj, expect_type) for obj in parsed] + + @classmethod + def json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: + return cls.json_list(data, expect_type, stripped, True)[0] + + @classmethod + def xml(cls, data: bytes, expect_type: Type[T], stripped: bool) -> T: + cls.check_type_supportance(expect_type) + try: + xml_data = io.BytesIO(data) + rv = read_aas_xml_element(xml_data, cls.type_constructables_map[expect_type], + stripped=stripped, failsafe=False) + except (KeyError, ValueError) as e: + # xml deserialization creates an error chain. since we only return one error, return the root cause + f: BaseException = e + while f.__cause__ is not None: + f = f.__cause__ + raise UnprocessableEntity(str(f)) from e + except (etree.XMLSyntaxError, model.AASConstraintViolation) as e: + raise UnprocessableEntity(str(e)) from e + return cls.assert_type(rv, expect_type) + + @classmethod + def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> T: + """ + TODO: werkzeug documentation recommends checking the content length before retrieving the body to prevent + running out of memory. but it doesn't state how to check the content length + also: what would be a reasonable maximum content length? the request body isn't limited by the xml/json + schema + In the meeting (25.11.2020) we discussed, this may refer to a reverse proxy in front of this WSGI app, + which should limit the maximum content length. + """ + valid_content_types = ("application/json", "application/xml", "text/xml") + + if request.mimetype not in valid_content_types: + raise werkzeug.exceptions.UnsupportedMediaType( + f"Invalid content-type: {request.mimetype}! Supported types: " + + ", ".join(valid_content_types)) + + if request.mimetype == "application/json": + return cls.json(request.get_data(), expect_type, stripped) + return cls.xml(request.get_data(), expect_type, stripped) class IdentifierConverter(werkzeug.routing.UnicodeConverter): @@ -348,72 +390,70 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v1", [ - Submount("/shells//aas", [ - Rule("/", methods=["GET"], endpoint=self.get_aas), - Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/assetInformation", methods=["PUT"], endpoint=self.put_aas_asset_information), - Submount("/submodels", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("//", methods=["PUT"], - endpoint=self.put_aas_submodel_refs), - Rule("//", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific) + Submount("/shells", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_all), + Submount("/", [ + Rule("/", methods=["GET"], endpoint=self.get_aas), + Rule("/", methods=["PUT"], endpoint=self.put_aas), + Rule("/", methods=["DELETE"], endpoint=self.delete_aas), + Submount("/aas", [ + Rule("/", methods=["GET"], endpoint=self.get_aas), + Rule("/", methods=["PUT"], endpoint=self.put_aas), + Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/assetInformation", methods=["PUT"], endpoint=self.put_aas_asset_information), + Submount("/submodels", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("//", methods=["PUT"], + endpoint=self.put_aas_submodel_refs), + Rule("//", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific) + ]) + ]) ]) ]), - Submount("/submodels//submodel", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Submount("/submodelElements", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Submount("/", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_id_short_path), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_id_short_path), - # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] - # see https://github.com/python/mypy/issues/5374 - Rule("/values/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("/values/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.SubmodelElementCollection, "value")), # type: ignore - Rule("/annotations/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation")), - Rule("/annotations/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr( - model.AnnotatedRelationshipElement, "annotation", - request_body_type=model.DataElement)), # type: ignore - Rule("/statements/", methods=["GET"], - endpoint=self.factory_get_submodel_submodel_elements_nested_attr(model.Entity, - "statement")), - Rule("/statements/", methods=["POST"], - endpoint=self.factory_post_submodel_submodel_elements_nested_attr(model.Entity, - "statement")), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), + Submount("/submodels", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_all), + Submount("/", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel), + Rule("/", methods=["PUT"], endpoint=self.put_submodel), + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), + Submount("/submodel", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel), + Rule("/", methods=["PUT"], endpoint=self.put_submodel), + Submount("/submodelElements", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Submount("/", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_id_short_path), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_id_short_path), + Submount("/constraints", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) + ]), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) ]) - ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), ]) ]) ]) @@ -433,6 +473,11 @@ def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._ raise NotFound(f"No {type_.__name__} with {identifier} found!") return identifiable + def _get_all_obj_of_type(self, type_: Type[model.provider._IT]) -> Iterator[model.provider._IT]: + for obj in self.object_store: + if isinstance(obj, type_): + yield obj + def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: try: return reference.resolve(self.object_store) @@ -503,18 +548,39 @@ def handle_request(self, request: Request): except werkzeug.exceptions.NotAcceptable as e: return e + # ------ AAS REPO ROUTES ------- + def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) + id_short = request.args.get("idShort") + if id_short is not None: + aas = filter(lambda shell: shell.id_short == id_short, aas) + asset_ids = request.args.get("assetIds") + if asset_ids is not None: + kv_pairs = HTTPApiDecoder.json_list(asset_ids, model.IdentifierKeyValuePair, False, False) + # TODO: it's currently unclear how to filter with these IdentifierKeyValuePairs + return response_t(list(aas)) + + def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + return response_t() + # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content parameter response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() return response_t(aas, stripped=is_stripped_request(request)) def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content parameter response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - aas.update_from(parse_request_body(request, model.AssetAdministrationShell)) + aas.update_from(HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, + is_stripped_request(request))) aas.commit() return response_t() @@ -528,7 +594,7 @@ def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - aas.asset_information = parse_request_body(request, model.AssetInformation) + aas.asset_information = HTTPApiDecoder.request_body(request, model.AssetInformation, False) aas.commit() return response_t() @@ -543,7 +609,7 @@ def put_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - sm_ref = parse_request_body(request, model.AASReference) + sm_ref = HTTPApiDecoder.request_body(request, model.AASReference, is_stripped_request(request)) if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") aas.submodel.add(sm_ref) @@ -563,6 +629,22 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** aas.commit() return response_t() + # ------ SUBMODEL REPO ROUTES ------- + def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + id_short = request.args.get("idShort") + if id_short is not None: + submodels = filter(lambda sm: sm.id_short == id_short, submodels) + # TODO: filter by semantic id + # semantic_id = request.args.get("semanticId") + return response_t(list(submodels)) + + def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.Submodel)) + return response_t() + # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters @@ -575,12 +657,13 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - submodel.update_from(parse_request_body(request, model.Submodel)) + submodel.update_from(HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request))) submodel.commit() return response_t() def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent, semanticId, parentPath parameters + # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec + # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() @@ -595,7 +678,7 @@ def get_submodel_submodel_elements_id_short_path(self, request: Request, url_arg return response_t(submodel_element, stripped=is_stripped_request(request)) def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content parameter + # TODO: support content, extent parameter response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] submodel = self._get_obj_ts(submodel_identifier, model.Submodel) @@ -655,7 +738,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: submodel.update() id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) - qualifier = parse_request_body(request, model.Qualifier) + qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) if sm_or_se.qualifier.contains_id("type", qualifier.type): raise Conflict(f"Qualifier with type {qualifier.type} already exists!") sm_or_se.qualifier.add(qualifier) @@ -675,7 +758,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: submodel.update() id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) - new_qualifier = parse_request_body(request, model.Qualifier) + new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) qualifier_type = url_args["qualifier_type"] try: qualifier = sm_or_se.get_qualifier_by_type(qualifier_type) @@ -713,45 +796,7 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg except KeyError: raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") sm_or_se.commit() - return response_t(Result(None)) - - # --------- SUBMODEL ROUTE FACTORIES --------- - def factory_get_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str) \ - -> Callable[[Request, Dict], Response]: - def route(request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) - if not isinstance(submodel_element, type_): - raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") - return response_t(Result(tuple(getattr(submodel_element, attr)))) - return route - - def factory_post_submodel_submodel_elements_nested_attr(self, type_: Type[model.Referable], attr: str, - request_body_type: Type[model.SubmodelElement] - = model.SubmodelElement) \ - -> Callable[[Request, Dict, MapAdapter], Response]: - def route(request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() - id_shorts = url_args["id_shorts"] - submodel_element = self._get_nested_submodel_element(submodel, id_shorts) - if not isinstance(submodel_element, type_): - raise UnprocessableEntity(f"Submodel element {submodel_element} is not a(n) {type_.__name__}!") - new_submodel_element = parse_request_body(request, request_body_type) - if getattr(submodel_element, attr).contains_id("id_short", new_submodel_element.id_short): - raise Conflict(f"Submodel element with id_short {new_submodel_element.id_short} already exists!") - getattr(submodel_element, attr).add(new_submodel_element) - submodel_element.commit() - created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_specific_nested, { - "submodel_id": submodel_identifier, - "id_shorts": id_shorts + [new_submodel_element.id_short] - }, force_external=True) - return response_t(Result(new_submodel_element), status=201, headers={"Location": created_resource_url}) - return route + return response_t() if __name__ == "__main__": From 7ee258f6c1300a15b0c03a85d9d835925a198de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 20 Sep 2021 18:00:17 +0200 Subject: [PATCH 082/157] adapter.http: add missing stripped parameter to HTTPApiDecoder.request_body() call --- basyx/aas/adapter/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d908fd251..22dd4c188 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -690,7 +690,8 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg ) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement) # type: ignore + new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, + is_stripped_request(request)) # type: ignore try: submodel_element = parent.get_referable(id_short_path[-1]) except KeyError: From 3c3f2a2f2c35e191dab43b374b8584781ad91255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 21 Sep 2021 18:16:06 +0200 Subject: [PATCH 083/157] adapter.http: update to Review3 add Base64UrlJsonConverter for json objects encoded as base64url as part of the url support deserializing submodels in HTTPApiDecoder add POST routes, remove 'create' functionality from PUT routes --- basyx/aas/adapter/http.py | 160 +++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 47 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 22dd4c188..436fd4376 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -245,6 +245,7 @@ class HTTPApiDecoder: model.AASReference: XMLConstructables.AAS_REFERENCE, model.IdentifierKeyValuePair: XMLConstructables.IDENTIFIER_KEY_VALUE_PAIR, model.Qualifier: XMLConstructables.QUALIFIER, + model.Submodel: XMLConstructables.SUBMODEL, model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT } @@ -338,6 +339,35 @@ def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> return cls.xml(request.get_data(), expect_type, stripped) +class Base64UrlJsonConverter(werkzeug.routing.UnicodeConverter): + encoding = "utf-8" + + def __init__(self, url_map, t: str): + super().__init__(url_map) + self.type: type + if t == "AASReference": + self.type = model.AASReference + else: + raise ValueError(f"invalid value t={t}") + + def to_url(self, value: object) -> str: + return super().to_url(base64.urlsafe_b64encode(json.dumps(value, cls=AASToJsonEncoder).encode(self.encoding))) + + def to_python(self, value: str) -> object: + value = super().to_python(value) + try: + decoded = base64.urlsafe_b64decode(super().to_python(value)).decode(self.encoding) + except binascii.Error: + raise BadRequest(f"Encoded json object {value} is invalid base64url!") + except UnicodeDecodeError: + raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + + try: + return HTTPApiDecoder.json(decoded, self.type, False) + except json.JSONDecodeError: + raise BadRequest(f"{decoded} is not a valid json string!") + + class IdentifierConverter(werkzeug.routing.UnicodeConverter): encoding = "utf-8" @@ -392,6 +422,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/api/v1", [ Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), + Rule("/", methods=["POST"], endpoint=self.post_aas), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), @@ -399,13 +430,12 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/aas", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/assetInformation", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/assetInformation", methods=["PUT"], endpoint=self.put_aas_asset_information), + Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("//", methods=["PUT"], - endpoint=self.put_aas_submodel_refs), - Rule("//", methods=["DELETE"], + Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]) ]) @@ -413,6 +443,7 @@ def __init__(self, object_store: model.AbstractObjectStore): ]), Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_all), + Rule("/", methods=["POST"], endpoint=self.post_submodel), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), @@ -420,11 +451,15 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodel", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Submount("/submodelElements", [ + Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_elements_id_short_path), Rule("/", methods=["DELETE"], @@ -442,17 +477,17 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.delete_submodel_submodel_element_constraints), ]) ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), - ]) + ]), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), ]) ]) ]) @@ -460,7 +495,8 @@ def __init__(self, object_store: model.AbstractObjectStore): ]) ], converters={ "identifier": IdentifierConverter, - "id_short_path": IdShortPathConverter + "id_short_path": IdShortPathConverter, + "base64url_json": Base64UrlJsonConverter }) def __call__(self, environ, start_response): @@ -484,15 +520,6 @@ def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> m except (KeyError, TypeError, model.UnexpectedTypeError) as e: raise werkzeug.exceptions.InternalServerError(str(e)) from e - @classmethod - def _get_aas_submodel_reference_by_submodel_identifier(cls, aas: model.AssetAdministrationShell, - sm_identifier: model.Identifier) \ - -> model.AASReference[model.Submodel]: - for sm_ref in aas.submodel: - if sm_ref.get_identifier() == sm_identifier: - return sm_ref - raise NotFound(f"No reference to submodel with {sm_identifier} found!") - @classmethod def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, id_shorts: List[str]) \ -> model.SubmodelElement: @@ -561,6 +588,19 @@ def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: it's currently unclear how to filter with these IdentifierKeyValuePairs return response_t(list(aas)) + def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + aas = HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, False) + try: + self.object_store.add(aas) + except KeyError as e: + raise Conflict(f"AssetAdministrationShell with Identifier {aas.identification} already exists!") from e + aas.commit() + created_resource_url = map_adapter.build(self.get_aas, { + "aas_id": aas.identification + }, force_external=True) + return response_t(aas, status=201, headers={"Location": created_resource_url}) + def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) @@ -604,12 +644,12 @@ def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas.update() return response_t(list(aas.submodel)) - def put_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - sm_ref = HTTPApiDecoder.request_body(request, model.AASReference, is_stripped_request(request)) + sm_ref = HTTPApiDecoder.request_body(request, model.AASReference, False) if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") aas.submodel.add(sm_ref) @@ -620,14 +660,12 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - sm_ref = self._get_aas_submodel_reference_by_submodel_identifier(aas, url_args["sm_id"]) - # use remove(sm_ref) because it raises a KeyError if sm_ref is not present - # sm_ref must be present because _get_aas_submodel_reference_by_submodel_identifier() found it there - # so if sm_ref is not in aas.submodel, this implementation is bugged and the raised KeyError will result - # in an InternalServerError - aas.submodel.remove(sm_ref) - aas.commit() - return response_t() + for sm_ref in aas.submodel: + if sm_ref == url_args["submodel_ref"]: + aas.submodel.remove(sm_ref) + aas.commit() + return response_t() + raise NotFound(f"The AAS {aas!r} doesn't have the reference {url_args['submodel_ref']!r}!") # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -640,6 +678,19 @@ def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Respo # semantic_id = request.args.get("semanticId") return response_t(list(submodels)) + def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) + try: + self.object_store.add(submodel) + except KeyError as e: + raise Conflict(f"Submodel with Identifier {submodel.identification} already exists!") from e + submodel.commit() + created_resource_url = map_adapter.build(self.get_submodel, { + "submodel_id": submodel.identification + }, force_external=True) + return response_t(submodel, status=201, headers={"Location": created_resource_url}) + def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.Submodel)) @@ -677,27 +728,42 @@ def get_submodel_submodel_elements_id_short_path(self, request: Request, url_arg submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(submodel_element, stripped=is_stripped_request(request)) - def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] submodel = self._get_obj_ts(submodel_identifier, model.Submodel) submodel.update() - id_short_path = url_args["id_shorts"] - parent = self._expect_namespace( - self._get_nested_submodel_element(submodel, url_args["id_shorts"][:-1]), - id_short_path[-1] - ) + id_short_path = url_args.get("id_shorts", []) + parent = self._get_submodel_or_nested_submodel_element(submodel, id_short_path) + if not isinstance(parent, model.UniqueIdShortNamespace): + raise BadRequest(f"{parent!r} is not a namespace, can't add child submodel element!") # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, is_stripped_request(request)) # type: ignore try: - submodel_element = parent.get_referable(id_short_path[-1]) - except KeyError: parent.add_referable(new_submodel_element) - new_submodel_element.commit() - return response_t(new_submodel_element, status=201) + except KeyError: + raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " + f"within {parent}!") + created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { + "submodel_id": submodel.identification, + "id_shorts": id_short_path + [new_submodel_element.id_short] + }, force_external=True) + return response_t(new_submodel_element, status=201, headers={"Location": created_resource_url}) + + def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameter + response_t = get_response_type(request) + submodel_identifier = url_args["submodel_id"] + submodel = self._get_obj_ts(submodel_identifier, model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] + # see https://github.com/python/mypy/issues/5374 + new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, + is_stripped_request(request)) # type: ignore submodel_element.update_from(new_submodel_element) submodel_element.commit() return response_t() From fa1c1485ade976c1ef38df169e866da2343d3939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:42:22 +0100 Subject: [PATCH 084/157] adapter._generic: remove `identifier_uri_(de/en)code` functions --- basyx/aas/adapter/_generic.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/basyx/aas/adapter/_generic.py b/basyx/aas/adapter/_generic.py index a65cf54e5..34c3412b1 100644 --- a/basyx/aas/adapter/_generic.py +++ b/basyx/aas/adapter/_generic.py @@ -11,7 +11,6 @@ from typing import Dict, Type from basyx.aas import model -import urllib.parse # XML Namespace definition XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"} @@ -114,18 +113,3 @@ KEY_TYPES_CLASSES_INVERSE: Dict[model.KeyTypes, Type[model.Referable]] = \ {v: k for k, v in model.KEY_TYPES_CLASSES.items()} - - -def identifier_uri_encode(id_: model.Identifier) -> str: - return IDENTIFIER_TYPES[id_.id_type] + ":" + urllib.parse.quote(id_.id, safe="") - - -def identifier_uri_decode(id_str: str) -> model.Identifier: - try: - id_type_str, id_ = id_str.split(":", 1) - except ValueError as e: - raise ValueError(f"Identifier '{id_str}' is not of format 'ID_TYPE:ID'") - id_type = IDENTIFIER_TYPES_INVERSE.get(id_type_str) - if id_type is None: - raise ValueError(f"Identifier Type '{id_type_str}' is invalid") - return model.Identifier(urllib.parse.unquote(id_), id_type) From b4d82d0a010441cc4f97a71e38c7652fbe4e59b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:47:34 +0100 Subject: [PATCH 085/157] adapter.http: update for changes made in the last 2 years This commit consists mostly of renamed variables, but also other stuff like the identifiers, which consisted of an IdentifierType and the actual Identifier previously. --- basyx/aas/adapter/http.py | 59 ++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 436fd4376..2385f74ef 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -25,10 +25,10 @@ from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response -from aas import model +from basyx.aas import model +from ._generic import XML_NS_MAP from .xml import XMLConstructables, read_aas_xml_element, xml_serialization from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder -from ._generic import IDENTIFIER_TYPES, IDENTIFIER_TYPES_INVERSE from typing import Callable, Dict, Iterator, List, Optional, Type, TypeVar, Union @@ -129,17 +129,17 @@ def __init__(self, *args, content_type="application/xml", **kwargs): def serialize(self, obj: ResponseData, stripped: bool) -> str: # TODO: xml serialization doesn't support stripped objects if isinstance(obj, Result): - response_elem = result_to_xml(obj, nsmap=xml_serialization.NS_MAP) + response_elem = result_to_xml(obj, nsmap=XML_NS_MAP) etree.cleanup_namespaces(response_elem) else: if isinstance(obj, list): - response_elem = etree.Element("list", nsmap=xml_serialization.NS_MAP) + response_elem = etree.Element("list", nsmap=XML_NS_MAP) for obj in obj: response_elem.append(aas_object_to_xml(obj)) etree.cleanup_namespaces(response_elem) else: # dirty hack to be able to use the namespace prefixes defined in xml_serialization.NS_MAP - parent = etree.Element("parent", nsmap=xml_serialization.NS_MAP) + parent = etree.Element("parent", nsmap=XML_NS_MAP) response_elem = aas_object_to_xml(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) @@ -242,8 +242,8 @@ class HTTPApiDecoder: type_constructables_map = { model.AssetAdministrationShell: XMLConstructables.ASSET_ADMINISTRATION_SHELL, model.AssetInformation: XMLConstructables.ASSET_INFORMATION, - model.AASReference: XMLConstructables.AAS_REFERENCE, - model.IdentifierKeyValuePair: XMLConstructables.IDENTIFIER_KEY_VALUE_PAIR, + model.ModelReference: XMLConstructables.MODEL_REFERENCE, + model.SpecificAssetId: XMLConstructables.SPECIFIC_ASSET_ID, model.Qualifier: XMLConstructables.QUALIFIER, model.Submodel: XMLConstructables.SUBMODEL, model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT @@ -279,13 +279,13 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool # that automatically constructor: Optional[Callable[..., T]] = None args = [] - if expect_type is model.AASReference: + if expect_type is model.ModelReference: constructor = decoder._construct_aas_reference # type: ignore args.append(model.Submodel) elif expect_type is model.AssetInformation: constructor = decoder._construct_asset_information # type: ignore - elif expect_type is model.IdentifierKeyValuePair: - constructor = decoder._construct_identifier_key_value_pair # type: ignore + elif expect_type is model.SpecificAssetId: + constructor = decoder._construct_specific_asset_id # type: ignore if constructor is not None: # construct elements that aren't self-identified @@ -346,7 +346,7 @@ def __init__(self, url_map, t: str): super().__init__(url_map) self.type: type if t == "AASReference": - self.type = model.AASReference + self.type = model.ModelReference else: raise ValueError(f"invalid value t={t}") @@ -372,8 +372,7 @@ class IdentifierConverter(werkzeug.routing.UnicodeConverter): encoding = "utf-8" def to_url(self, value: model.Identifier) -> str: - return super().to_url(base64.urlsafe_b64encode((IDENTIFIER_TYPES[value.id_type] + ":" + value.id) - .encode(self.encoding))) + return super().to_url(base64.urlsafe_b64encode(value.encode(self.encoding))) def to_python(self, value: str) -> model.Identifier: value = super().to_python(value) @@ -383,11 +382,7 @@ def to_python(self, value: str) -> model.Identifier: raise BadRequest(f"Encoded identifier {value} is invalid base64url!") except UnicodeDecodeError: raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") - id_type_str, id_ = decoded.split(":", 1) - try: - return model.Identifier(id_, IDENTIFIER_TYPES_INVERSE[id_type_str]) - except KeyError: - raise BadRequest(f"{id_type_str} is not a valid identifier type!") + return decoded class IdShortPathConverter(werkzeug.routing.UnicodeConverter): @@ -514,7 +509,7 @@ def _get_all_obj_of_type(self, type_: Type[model.provider._IT]) -> Iterator[mode if isinstance(obj, type_): yield obj - def _resolve_reference(self, reference: model.AASReference[model.base._RT]) -> model.base._RT: + def _resolve_reference(self, reference: model.ModelReference[model.base._RT]) -> model.base._RT: try: return reference.resolve(self.object_store) except (KeyError, TypeError, model.UnexpectedTypeError) as e: @@ -584,8 +579,8 @@ def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: aas = filter(lambda shell: shell.id_short == id_short, aas) asset_ids = request.args.get("assetIds") if asset_ids is not None: - kv_pairs = HTTPApiDecoder.json_list(asset_ids, model.IdentifierKeyValuePair, False, False) - # TODO: it's currently unclear how to filter with these IdentifierKeyValuePairs + spec_asset_ids = HTTPApiDecoder.json_list(asset_ids, model.SpecificAssetId, False, False) + # TODO: it's currently unclear how to filter with these SpecificAssetIds return response_t(list(aas)) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: @@ -594,10 +589,10 @@ def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> try: self.object_store.add(aas) except KeyError as e: - raise Conflict(f"AssetAdministrationShell with Identifier {aas.identification} already exists!") from e + raise Conflict(f"AssetAdministrationShell with Identifier {aas.id} already exists!") from e aas.commit() created_resource_url = map_adapter.build(self.get_aas, { - "aas_id": aas.identification + "aas_id": aas.id }, force_external=True) return response_t(aas, status=201, headers={"Location": created_resource_url}) @@ -649,7 +644,7 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas_identifier = url_args["aas_id"] aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) aas.update() - sm_ref = HTTPApiDecoder.request_body(request, model.AASReference, False) + sm_ref = HTTPApiDecoder.request_body(request, model.ModelReference, False) if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") aas.submodel.add(sm_ref) @@ -684,10 +679,10 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte try: self.object_store.add(submodel) except KeyError as e: - raise Conflict(f"Submodel with Identifier {submodel.identification} already exists!") from e + raise Conflict(f"Submodel with Identifier {submodel.id} already exists!") from e submodel.commit() created_resource_url = map_adapter.build(self.get_submodel, { - "submodel_id": submodel.identification + "submodel_id": submodel.id }, force_external=True) return response_t(submodel, status=201, headers={"Location": created_resource_url}) @@ -740,15 +735,15 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar raise BadRequest(f"{parent!r} is not a namespace, can't add child submodel element!") # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, - is_stripped_request(request)) # type: ignore + new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, # type: ignore + is_stripped_request(request)) try: parent.add_referable(new_submodel_element) except KeyError: raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " f"within {parent}!") created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { - "submodel_id": submodel.identification, + "submodel_id": submodel.id, "id_shorts": id_short_path + [new_submodel_element.id_short] }, force_external=True) return response_t(new_submodel_element, status=201, headers={"Location": created_resource_url}) @@ -762,8 +757,8 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, - is_stripped_request(request)) # type: ignore + new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, # type: ignore + is_stripped_request(request)) submodel_element.update_from(new_submodel_element) submodel_element.commit() return response_t() @@ -869,5 +864,5 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg if __name__ == "__main__": from werkzeug.serving import run_simple # use example_aas_missing_attributes, because the AAS from example_aas has no views - from aas.examples.data.example_aas import create_full_example + from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From 27b6218f3b1e8e1a80dafe130a2e6de01804b6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:50:20 +0100 Subject: [PATCH 086/157] test.adapter.http: update w.r.t. the changes of the last 2 years --- test/adapter/test_http.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index d62c8a1ef..88a10b05d 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -35,15 +35,15 @@ import random import werkzeug.urls -from aas import model -from aas.adapter.http import WSGIApp, identifier_uri_encode -from aas.examples.data.example_aas import create_full_example +from basyx.aas import model +from basyx.aas.adapter.http import WSGIApp +from basyx.aas.examples.data.example_aas import create_full_example from typing import Set def _encode_and_quote(identifier: model.Identifier) -> str: - return werkzeug.urls.url_quote(werkzeug.urls.url_quote(identifier_uri_encode(identifier), safe=""), safe="") + return werkzeug.urls.url_quote(werkzeug.urls.url_quote(identifier, safe=""), safe="") def _check_transformed(response, case): @@ -79,9 +79,9 @@ def _check_transformed(response, case): # store identifiers of available AAS and Submodels for obj in create_full_example(): if isinstance(obj, model.AssetAdministrationShell): - IDENTIFIER_AAS.add(_encode_and_quote(obj.identification)) + IDENTIFIER_AAS.add(_encode_and_quote(obj.id)) if isinstance(obj, model.Submodel): - IDENTIFIER_SUBMODEL.add(_encode_and_quote(obj.identification)) + IDENTIFIER_SUBMODEL.add(_encode_and_quote(obj.id)) # load aas and submodel api specs AAS_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-aas.yaml", From cdf1765f6d3f8b1c7c1eab36f510c6ee54f4d391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:52:06 +0100 Subject: [PATCH 087/157] adapter.http: ignore the type of some imports to make `mypy` happy --- basyx/aas/adapter/http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 2385f74ef..c7a3a9f93 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -18,12 +18,12 @@ import io import json from lxml import etree # type: ignore -import werkzeug.exceptions -import werkzeug.routing -import werkzeug.urls +import werkzeug.exceptions # type: ignore +import werkzeug.routing # type: ignore +import werkzeug.urls # type: ignore from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount -from werkzeug.wrappers import Request, Response +from werkzeug.wrappers import Request, Response # type: ignore from basyx.aas import model from ._generic import XML_NS_MAP @@ -862,7 +862,7 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg if __name__ == "__main__": - from werkzeug.serving import run_simple + from werkzeug.serving import run_simple # type: ignore # use example_aas_missing_attributes, because the AAS from example_aas has no views from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From 720ca3f32af9d509de54f9e9174ee263acf9c90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:52:38 +0100 Subject: [PATCH 088/157] test.adapter.http: ignore the type of an import to make `mypy` happy --- test/adapter/test_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 88a10b05d..88cf314cd 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -33,7 +33,7 @@ import schemathesis import hypothesis.strategies import random -import werkzeug.urls +import werkzeug.urls # type: ignore from basyx.aas import model from basyx.aas.adapter.http import WSGIApp From c4b564d470f4164bb6dc48e5036637f95541ede7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 22:53:28 +0100 Subject: [PATCH 089/157] adapter.http: remove an outdated comment Views were removed from the spec and we're no longer using `example_aas_missing_attributes()` anyway. --- basyx/aas/adapter/http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index c7a3a9f93..99064b6ef 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -863,6 +863,5 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg if __name__ == "__main__": from werkzeug.serving import run_simple # type: ignore - # use example_aas_missing_attributes, because the AAS from example_aas has no views from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From 3bf02d5a5192e5eca873cdb861a22ef0b8dfbbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 11 Dec 2023 23:18:33 +0100 Subject: [PATCH 090/157] adapter.http: rename occurances of `AASReference` to `ModelReference` --- basyx/aas/adapter/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 99064b6ef..fb631a27b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -275,7 +275,7 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool raise UnprocessableEntity(f"Expected a single object of type {expect_type.__name__}, got {parsed!r}!") # TODO: the following is ugly, but necessary because references aren't self-identified objects # in the json schema - # TODO: json deserialization will always create an AASReference[Submodel], xml deserialization determines + # TODO: json deserialization will always create an ModelReference[Submodel], xml deserialization determines # that automatically constructor: Optional[Callable[..., T]] = None args = [] @@ -345,7 +345,7 @@ class Base64UrlJsonConverter(werkzeug.routing.UnicodeConverter): def __init__(self, url_map, t: str): super().__init__(url_map) self.type: type - if t == "AASReference": + if t == "ModelReference": self.type = model.ModelReference else: raise ValueError(f"invalid value t={t}") @@ -430,7 +430,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]) ]) From 0b13c18e0bc9f14bc7478d0f258cfae89acd52cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 19:54:53 +0100 Subject: [PATCH 091/157] adapter.http: update Werkzeug to 3.x --- requirements.txt | 2 +- setup.py | 2 +- test/adapter/test_http.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9a5813f34..9a5de186a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ python-dateutil>=2.8,<3.0 types-python-dateutil pyecma376-2>=0.2.4 urllib3>=1.26,<2.0 -Werkzeug>=1.0.1,<2 +Werkzeug~=3.0 schemathesis~=3.7 hypothesis~=6.13 diff --git a/setup.py b/setup.py index e50c50624..274d553c1 100755 --- a/setup.py +++ b/setup.py @@ -46,6 +46,6 @@ 'lxml>=4.2,<5', 'urllib3>=1.26,<2.0', 'pyecma376-2>=0.2.4', - 'Werkzeug>=1.0.1,<2' + 'Werkzeug~=3.0' ] ) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 88cf314cd..3b53eb9c2 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -30,10 +30,11 @@ import os import pathlib +import urllib.parse + import schemathesis import hypothesis.strategies import random -import werkzeug.urls # type: ignore from basyx.aas import model from basyx.aas.adapter.http import WSGIApp @@ -43,7 +44,7 @@ def _encode_and_quote(identifier: model.Identifier) -> str: - return werkzeug.urls.url_quote(werkzeug.urls.url_quote(identifier, safe=""), safe="") + return urllib.parse.quote(urllib.parse.quote(identifier, safe=""), safe="") def _check_transformed(response, case): From 4185ec74c392c18462f8cb001ddcfbf78039e928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 19:56:08 +0100 Subject: [PATCH 092/157] adapter.http: allow typechecking werkzeug imports --- basyx/aas/adapter/http.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index fb631a27b..65ce5d567 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -9,6 +9,9 @@ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. +# TODO: remove this once the werkzeug type annotations have been fixed +# https://github.com/pallets/werkzeug/issues/2836 +# mypy: disable-error-code="arg-type" import abc import base64 @@ -17,20 +20,21 @@ import enum import io import json + from lxml import etree # type: ignore -import werkzeug.exceptions # type: ignore -import werkzeug.routing # type: ignore -import werkzeug.urls # type: ignore +import werkzeug.exceptions +import werkzeug.routing +import werkzeug.urls from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount -from werkzeug.wrappers import Request, Response # type: ignore +from werkzeug.wrappers import Request, Response from basyx.aas import model from ._generic import XML_NS_MAP from .xml import XMLConstructables, read_aas_xml_element, xml_serialization from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder -from typing import Callable, Dict, Iterator, List, Optional, Type, TypeVar, Union +from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union # TODO: support the path/reference/etc. parameter @@ -49,12 +53,12 @@ def __str__(self): class Message: - def __init__(self, code: str, text: str, type_: MessageType = MessageType.UNDEFINED, + def __init__(self, code: str, text: str, message_type: MessageType = MessageType.UNDEFINED, timestamp: Optional[datetime.datetime] = None): - self.code = code - self.text = text - self.messageType = type_ - self.timestamp = timestamp if timestamp is not None else datetime.datetime.utcnow() + self.code: str = code + self.text: str = text + self.message_type: MessageType = message_type + self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.utcnow() class Result: @@ -76,7 +80,7 @@ def _result_to_json(cls, result: Result) -> Dict[str, object]: @classmethod def _message_to_json(cls, message: Message) -> Dict[str, object]: return { - "messageType": message.messageType, + "messageType": message.message_type, "text": message.text, "code": message.code, "timestamp": message.timestamp.isoformat() @@ -167,7 +171,7 @@ def result_to_xml(result: Result, **kwargs) -> etree.Element: def message_to_xml(message: Message) -> etree.Element: message_elem = etree.Element("message") message_type_elem = etree.Element("messageType") - message_type_elem.text = str(message.messageType) + message_type_elem.text = str(message.message_type) text_elem = etree.Element("text") text_elem.text = message.text code_elem = etree.Element("code") @@ -494,8 +498,9 @@ def __init__(self, object_store: model.AbstractObjectStore): "base64url_json": Base64UrlJsonConverter }) - def __call__(self, environ, start_response): - response = self.handle_request(Request(environ)) + # TODO: the parameters can be typed via builtin wsgiref with Python 3.11+ + def __call__(self, environ, start_response) -> Iterable[bytes]: + response: Response = self.handle_request(Request(environ)) return response(environ, start_response) def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IT]) -> model.provider._IT: @@ -558,7 +563,7 @@ def handle_request(self, request: Request): endpoint, values = map_adapter.match() if endpoint is None: raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") - return endpoint(request, values, map_adapter=map_adapter) + return endpoint(request, values, map_adapter=map_adapter) # type: ignore[operator] # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them except werkzeug.exceptions.NotAcceptable as e: @@ -862,6 +867,6 @@ def delete_submodel_submodel_element_constraints(self, request: Request, url_arg if __name__ == "__main__": - from werkzeug.serving import run_simple # type: ignore + from werkzeug.serving import run_simple from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) From 380892526ab023e71d4ed1967e100b445e40c4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 19:58:11 +0100 Subject: [PATCH 093/157] test: disable http api tests for now --- test/adapter/test_http.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 3b53eb9c2..621f15c9d 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -126,8 +126,9 @@ def validate_response(self, response, case, additional_checks=()) -> None: # APIWorkflow.TestCase is a standard python unittest.TestCase -ApiTestAAS = APIWorkflowAAS.TestCase -ApiTestAAS.settings = HYPOTHESIS_SETTINGS +# TODO: Fix HTTP API Tests +# ApiTestAAS = APIWorkflowAAS.TestCase +# ApiTestAAS.settings = HYPOTHESIS_SETTINGS -ApiTestSubmodel = APIWorkflowSubmodel.TestCase -ApiTestSubmodel.settings = HYPOTHESIS_SETTINGS +# ApiTestSubmodel = APIWorkflowSubmodel.TestCase +# ApiTestSubmodel.settings = HYPOTHESIS_SETTINGS From 99f480e983cd1ea56038d1232b053ae529cd5eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 20:02:56 +0100 Subject: [PATCH 094/157] adapter.http: improve codestyle --- basyx/aas/adapter/http.py | 1 - test/adapter/test_http.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 65ce5d567..5fa2e2fb0 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -196,7 +196,6 @@ def aas_object_to_xml(obj: object) -> etree.Element: return xml_serialization.reference_to_xml(obj) if isinstance(obj, model.Submodel): return xml_serialization.submodel_to_xml(obj) - # TODO: xml serialization needs a constraint_to_xml() function if isinstance(obj, model.Qualifier): return xml_serialization.qualifier_to_xml(obj) if isinstance(obj, model.SubmodelElement): diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 621f15c9d..1dc849dc6 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -29,12 +29,12 @@ # TODO: add id_short format to schemata import os +import random import pathlib import urllib.parse import schemathesis import hypothesis.strategies -import random from basyx.aas import model from basyx.aas.adapter.http import WSGIApp From 03904a90c8ffcf5bd4826451686a59f7626f8eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 21:11:59 +0100 Subject: [PATCH 095/157] adapter.http: update license header --- basyx/aas/adapter/http.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 5fa2e2fb0..d422f5514 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -1,13 +1,9 @@ -# Copyright 2020 PyI40AAS Contributors +# Copyright (c) 2024 the Eclipse BaSyx Authors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: MIT # TODO: remove this once the werkzeug type annotations have been fixed # https://github.com/pallets/werkzeug/issues/2836 From 841b2fc009880ef92d593538a8666e11eff2b339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 14 Feb 2024 21:28:22 +0100 Subject: [PATCH 096/157] test.adapter.http: update license header --- test/adapter/test_http.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 1dc849dc6..528d2873b 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -1,13 +1,9 @@ -# Copyright 2021 PyI40AAS Contributors +# Copyright (c) 2024 the Eclipse BaSyx Authors # -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. +# SPDX-License-Identifier: MIT """ This test uses the schemathesis package to perform automated stateful testing on the implemented http api. Requests From c666488778a673dd215c2b37133d1594a046d9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 15 Feb 2024 14:49:31 +0100 Subject: [PATCH 097/157] adapter.http: document another 'type: ignore' comment --- basyx/aas/adapter/http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d422f5514..cb401dfc2 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: MIT # TODO: remove this once the werkzeug type annotations have been fixed -# https://github.com/pallets/werkzeug/issues/2836 +# https://github.com/pallets/werkzeug/issues/2836 # mypy: disable-error-code="arg-type" import abc @@ -558,6 +558,8 @@ def handle_request(self, request: Request): endpoint, values = map_adapter.match() if endpoint is None: raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") + # TODO: remove this 'type: ignore' comment once the werkzeug type annotations have been fixed + # https://github.com/pallets/werkzeug/issues/2836 return endpoint(request, values, map_adapter=map_adapter) # type: ignore[operator] # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them From 55b8f1c4d84be1b6b680142c6cb12f16fac3478a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 21 Feb 2024 18:09:21 +0100 Subject: [PATCH 098/157] adapter.http: remove `/aas` submount from AAS repository The submount has been removed from the spec, yay! --- basyx/aas/adapter/http.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index cb401dfc2..52c488c4b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -421,17 +421,13 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/", methods=["DELETE"], endpoint=self.delete_aas), - Submount("/aas", [ - Rule("/", methods=["GET"], endpoint=self.get_aas), - Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), - Submount("/submodels", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific) - ]) + Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), + Submount("/submodels", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("//", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific) ]) ]) ]), From e857a644584043e6ec67465ea4610bde74f2128e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 21 Feb 2024 18:15:09 +0100 Subject: [PATCH 099/157] adapter.http: update base URL from `/api/v1` to `/api/v3.0` --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index cb401dfc2..2db0cde82 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -413,7 +413,7 @@ class WSGIApp: def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ - Submount("/api/v1", [ + Submount("/api/v3.0", [ Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), Rule("/", methods=["POST"], endpoint=self.post_aas), From 712df72d55301c93c4113e8d5c7c4fc0417043d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 21 Feb 2024 19:00:04 +0100 Subject: [PATCH 100/157] adapter.http: remove hardcoded encoding from error messages --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index cb401dfc2..d40779a6e 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -380,7 +380,7 @@ def to_python(self, value: str) -> model.Identifier: except binascii.Error: raise BadRequest(f"Encoded identifier {value} is invalid base64url!") except UnicodeDecodeError: - raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + raise BadRequest(f"Encoded base64url value is not a valid {self.encoding} string!") return decoded From a9975f7945211abd5411e6b3061443a8f6b43236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 15:43:24 +0100 Subject: [PATCH 101/157] adapter.http: fix base64 decoding without padding Many online tools omit the padding when encoding base64url. Thus, requesters may omit the padding as well, which would cause the decoding to fail. Thus, we simply always append two padding characters, because python doesn't complain about too much padding. --- basyx/aas/adapter/http.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e55fddff3..73c0307d8 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -376,7 +376,11 @@ def to_url(self, value: model.Identifier) -> str: def to_python(self, value: str) -> model.Identifier: value = super().to_python(value) try: - decoded = base64.urlsafe_b64decode(super().to_python(value)).decode(self.encoding) + # If the requester omits the base64 padding, an exception will be raised. + # However, Python doesn't complain about too much padding, + # thus we simply always append two padding characters (==). + # See also: https://stackoverflow.com/a/49459036/4780052 + decoded = base64.urlsafe_b64decode(super().to_python(value) + "==").decode(self.encoding) except binascii.Error: raise BadRequest(f"Encoded identifier {value} is invalid base64url!") except UnicodeDecodeError: From 5bfbad301ee903ea8bc342b97cade3392394774b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 18:18:28 +0100 Subject: [PATCH 102/157] adapter.http: remove `/submodel` submount from Submodel repository --- basyx/aas/adapter/http.py | 68 ++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e55fddff3..6601befcc 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -438,47 +438,43 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), - Submount("/submodel", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Submount("/submodel-elements", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Submount("/submodel-elements", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), + Submount("/", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_elements_id_short_path), - Submount("/", [ + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_id_short_path), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_id_short_path), + Submount("/constraints", [ Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path), + endpoint=self.get_submodel_submodel_element_constraints), Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_id_short_path), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_id_short_path), - Submount("/constraints", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), - ]) - ]), + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), + ]) ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), - ]) + ]), + Submount("/constraints", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_element_constraints), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_constraints), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_constraints), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_constraints), ]) ]) ]) From 71ff951e2211d9b7077b614d0394e66ebbaf1d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 18:38:40 +0100 Subject: [PATCH 103/157] adapter.http: use builtin id_short path resolution --- basyx/aas/adapter/http.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e55fddff3..f5dc7bcdd 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -514,17 +514,19 @@ def _resolve_reference(self, reference: model.ModelReference[model.base._RT]) -> @classmethod def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, id_shorts: List[str]) \ -> model.SubmodelElement: - current_namespace: Union[model.UniqueIdShortNamespace, model.SubmodelElement] = namespace - for id_short in id_shorts: - current_namespace = cls._expect_namespace(current_namespace, id_short) - next_obj = cls._namespace_submodel_element_op(current_namespace, current_namespace.get_referable, id_short) - if not isinstance(next_obj, model.SubmodelElement): - raise werkzeug.exceptions.InternalServerError(f"{next_obj}, child of {current_namespace!r}, " - f"is not a submodel element!") - current_namespace = next_obj - if not isinstance(current_namespace, model.SubmodelElement): + if not id_shorts: raise ValueError("No id_shorts specified!") - return current_namespace + + try: + ret = namespace.get_referable(id_shorts) + except KeyError as e: + raise NotFound(e.args[0]) + except (TypeError, ValueError) as e: + raise BadRequest(e.args[0]) + + if not isinstance(ret, model.SubmodelElement): + raise BadRequest(f"{ret!r} is not a submodel element!") + return ret @classmethod def _get_submodel_or_nested_submodel_element(cls, submodel: model.Submodel, id_shorts: List[str]) \ From 80651539898b5bc116fa597ba437d515cbcc8958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 18:49:55 +0100 Subject: [PATCH 104/157] adapter.http: simplify XML serialization ... by using (now) builtin functionality. --- basyx/aas/adapter/http.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e55fddff3..6914dea90 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -27,7 +27,7 @@ from basyx.aas import model from ._generic import XML_NS_MAP -from .xml import XMLConstructables, read_aas_xml_element, xml_serialization +from .xml import XMLConstructables, read_aas_xml_element, xml_serialization, object_to_xml_element from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union @@ -135,12 +135,12 @@ def serialize(self, obj: ResponseData, stripped: bool) -> str: if isinstance(obj, list): response_elem = etree.Element("list", nsmap=XML_NS_MAP) for obj in obj: - response_elem.append(aas_object_to_xml(obj)) + response_elem.append(object_to_xml_element(obj)) etree.cleanup_namespaces(response_elem) else: # dirty hack to be able to use the namespace prefixes defined in xml_serialization.NS_MAP parent = etree.Element("parent", nsmap=XML_NS_MAP) - response_elem = aas_object_to_xml(obj) + response_elem = object_to_xml_element(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") @@ -182,23 +182,6 @@ def message_to_xml(message: Message) -> etree.Element: return message_elem -def aas_object_to_xml(obj: object) -> etree.Element: - # TODO: a similar function should be implemented in the xml serialization - if isinstance(obj, model.AssetAdministrationShell): - return xml_serialization.asset_administration_shell_to_xml(obj) - if isinstance(obj, model.AssetInformation): - return xml_serialization.asset_information_to_xml(obj) - if isinstance(obj, model.Reference): - return xml_serialization.reference_to_xml(obj) - if isinstance(obj, model.Submodel): - return xml_serialization.submodel_to_xml(obj) - if isinstance(obj, model.Qualifier): - return xml_serialization.qualifier_to_xml(obj) - if isinstance(obj, model.SubmodelElement): - return xml_serialization.submodel_element_to_xml(obj) - raise TypeError(f"Serializing {type(obj).__name__} to XML is not supported!") - - def get_response_type(request: Request) -> Type[APIResponse]: response_types: Dict[str, Type[APIResponse]] = { "application/json": JsonResponse, From b392d490c511e5bfe2e9d63a06f5a6d9060fe841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 18:51:34 +0100 Subject: [PATCH 105/157] adapter.http: fix `ModelReference` json deserialization The http adapter still used the old `construct_aas_reference` function, which doesn't exist anymore. The error was masked due to a broad `type: ignore`. These are changed to only ignore assignment type errors. --- basyx/aas/adapter/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e55fddff3..1a73b6769 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -279,12 +279,12 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool constructor: Optional[Callable[..., T]] = None args = [] if expect_type is model.ModelReference: - constructor = decoder._construct_aas_reference # type: ignore + constructor = decoder._construct_model_reference # type: ignore[assignment] args.append(model.Submodel) elif expect_type is model.AssetInformation: - constructor = decoder._construct_asset_information # type: ignore + constructor = decoder._construct_asset_information # type: ignore[assignment] elif expect_type is model.SpecificAssetId: - constructor = decoder._construct_specific_asset_id # type: ignore + constructor = decoder._construct_specific_asset_id # type: ignore[assignment] if constructor is not None: # construct elements that aren't self-identified From ae8d5276a5fcbd43295e838692742563ac4c62bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 19:01:33 +0100 Subject: [PATCH 106/157] adapter.http: simplify `SubmodelElement` deletion We can always call `_get_submodel_or_nested_submodel_element`, as it returns the submodel itself if we don't pass it any id_shorts. --- basyx/aas/adapter/http.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e55fddff3..468e5deb0 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -767,12 +767,10 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() id_short_path: List[str] = url_args["id_shorts"] - parent: model.UniqueIdShortNamespace = submodel - if len(id_short_path) > 1: - parent = self._expect_namespace( - self._get_nested_submodel_element(submodel, id_short_path[:-1]), - id_short_path[-1] - ) + parent: model.UniqueIdShortNamespace = self._expect_namespace( + self._get_submodel_or_nested_submodel_element(submodel, id_short_path[:-1]), + id_short_path[-1] + ) self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) return response_t() From 2e00cff7566e43cffcfe47ce3a267189e69dd6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 19:14:46 +0100 Subject: [PATCH 107/157] adapter.http: skip validation of id_shorts in URLs created by us --- basyx/aas/adapter/http.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index e55fddff3..d5ad6e7fd 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -396,9 +396,6 @@ def validate_id_short(cls, id_short: str) -> bool: return True def to_url(self, value: List[str]) -> str: - for id_short in value: - if not self.validate_id_short(id_short): - raise ValueError(f"{id_short} is not a valid id_short!") return super().to_url(self.id_short_sep.join(id_short for id_short in value)) def to_python(self, value: str) -> List[str]: From 6667a41fdfd3250e41a0270ac652f18617cadadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 15 Mar 2024 19:15:28 +0100 Subject: [PATCH 108/157] adapter.http: use `Referable.validate_id_short()` to validate id_shorts ... instead of the previous way of creating a `MultiLanguageProperty` to validate id_shorts. --- basyx/aas/adapter/http.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d5ad6e7fd..b26b0ea10 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -387,21 +387,15 @@ def to_python(self, value: str) -> model.Identifier: class IdShortPathConverter(werkzeug.routing.UnicodeConverter): id_short_sep = "." - @classmethod - def validate_id_short(cls, id_short: str) -> bool: - try: - model.MultiLanguageProperty(id_short) - except model.AASConstraintViolation: - return False - return True - def to_url(self, value: List[str]) -> str: return super().to_url(self.id_short_sep.join(id_short for id_short in value)) def to_python(self, value: str) -> List[str]: id_shorts = super().to_python(value).split(self.id_short_sep) for id_short in id_shorts: - if not self.validate_id_short(id_short): + try: + model.Referable.validate_id_short(id_short) + except (ValueError, model.AASConstraintViolation): raise BadRequest(f"{id_short} is not a valid id_short!") return id_shorts From 251afe630fa219109f6470c5b4a7dc4f81c85a1d Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 11:46:48 +0100 Subject: [PATCH 109/157] fixing delete_submodels() --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7bda980f1..6966d98c6 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -662,7 +662,7 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.Submodel)) + self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) return response_t() # --------- SUBMODEL ROUTES --------- From 50780e09f69ad653fbc6bd5e4ce8063b52f81473 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 12:39:55 +0100 Subject: [PATCH 110/157] adapter.http: implement semanticID filtering --- basyx/aas/adapter/http.py | 43 +++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 6966d98c6..67ef0ee29 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -228,9 +228,12 @@ class HTTPApiDecoder: model.SpecificAssetId: XMLConstructables.SPECIFIC_ASSET_ID, model.Qualifier: XMLConstructables.QUALIFIER, model.Submodel: XMLConstructables.SUBMODEL, - model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT + model.SubmodelElement: XMLConstructables.SUBMODEL_ELEMENT, + model.Reference: XMLConstructables.REFERENCE } + encoding = "utf-8" + @classmethod def check_type_supportance(cls, type_: type): if type_ not in cls.type_constructables_map: @@ -242,6 +245,18 @@ def assert_type(cls, obj: object, type_: Type[T]) -> T: raise UnprocessableEntity(f"Object {obj!r} is not of type {type_.__name__}!") return obj + @classmethod + def base64_decode(cls, data: Union[str, bytes]) -> str: + try: + # If the requester omits the base64 padding, an exception will be raised. + # However, Python doesn't complain about too much padding, + # thus we simply always append two padding characters (==). + # See also: https://stackoverflow.com/a/49459036/4780052 + decoded = base64.urlsafe_b64decode(data + "==").decode(cls.encoding) + except binascii.Error: + raise BadRequest(f"Encoded data {str(data)} is invalid base64url!") + return decoded + @classmethod def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool) -> List[T]: cls.check_type_supportance(expect_type) @@ -268,6 +283,9 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool constructor = decoder._construct_asset_information # type: ignore[assignment] elif expect_type is model.SpecificAssetId: constructor = decoder._construct_specific_asset_id # type: ignore[assignment] + elif expect_type is model.Reference: + constructor = decoder._construct_model_reference # type: ignore[assignment] + args.append(model.Submodel) if constructor is not None: # construct elements that aren't self-identified @@ -278,10 +296,21 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool return [cls.assert_type(obj, expect_type) for obj in parsed] + @classmethod + def base64json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool)\ + -> List[T]: + data = cls.base64_decode(data) + return cls.json_list(data, expect_type, stripped, expect_single) + @classmethod def json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: return cls.json_list(data, expect_type, stripped, True)[0] + @classmethod + def base64json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: + data = cls.base64_decode(data) + return cls.json_list(data, expect_type, stripped, True)[0] + @classmethod def xml(cls, data: bytes, expect_type: Type[T], stripped: bool) -> T: cls.check_type_supportance(expect_type) @@ -553,7 +582,7 @@ def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: aas = filter(lambda shell: shell.id_short == id_short, aas) asset_ids = request.args.get("assetIds") if asset_ids is not None: - spec_asset_ids = HTTPApiDecoder.json_list(asset_ids, model.SpecificAssetId, False, False) + spec_asset_ids = HTTPApiDecoder.base64json_list(asset_ids, model.SpecificAssetId, False, False) # TODO: it's currently unclear how to filter with these SpecificAssetIds return response_t(list(aas)) @@ -643,9 +672,12 @@ def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Respo id_short = request.args.get("idShort") if id_short is not None: submodels = filter(lambda sm: sm.id_short == id_short, submodels) - # TODO: filter by semantic id - # semantic_id = request.args.get("semanticId") - return response_t(list(submodels)) + semantic_id = request.args.get("semanticId") + if semantic_id is not None: + if semantic_id is not None: + spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + return response_t(list(submodels), stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -665,7 +697,6 @@ def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Respon self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) return response_t() - # --------- SUBMODEL ROUTES --------- def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) From b0bdd7818f3fea2614604f2dbf81803664a0acb7 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 13:30:21 +0100 Subject: [PATCH 111/157] adapter.http: implement metadata Routes --- basyx/aas/adapter/http.py | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 67ef0ee29..2d862ab70 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -441,14 +441,18 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_all), Rule("/", methods=["POST"], endpoint=self.post_submodel), + Rule("/$metadata", methods=["GET"], endpoint=self.get_allsubmodels_metadata), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_elements_id_short_path), + Rule("/$metadata", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_metadata), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path), @@ -458,6 +462,8 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.put_submodel_submodel_elements_id_short_path), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_elements_id_short_path), + Rule("/$metadata", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), Submount("/constraints", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), @@ -692,6 +698,20 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte }, force_external=True) return response_t(submodel, status=201, headers={"Location": created_resource_url}) + def get_allsubmodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + id_short = request.args.get("idShort") + if id_short is not None: + submodels = filter(lambda sm: sm.id_short == id_short, submodels) + semantic_id = request.args.get("semanticId") + if semantic_id is not None: + spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + return response_t(list(submodels), stripped=True) + + # --------- SUBMODEL ROUTES --------- + def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) @@ -704,6 +724,12 @@ def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: submodel.update() return response_t(submodel, stripped=is_stripped_request(request)) + def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(submodel, stripped=True) + def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) @@ -720,6 +746,14 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kw submodel.update() return response_t(list(submodel.submodel_element)) + def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec + # TODO: support content, extent, semanticId parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return response_t(list(submodel.submodel_element), stripped=True) + def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) @@ -728,6 +762,15 @@ def get_submodel_submodel_elements_id_short_path(self, request: Request, url_arg submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(submodel_element, stripped=is_stripped_request(request)) + def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + # TODO: support content, extent parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + return response_t(submodel_element, stripped=True) + def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) From 9f94a11b875d22ba65cf2cd0dcb98ef9541037ea Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 20:31:17 +0100 Subject: [PATCH 112/157] adapter.http: implement reference routes --- basyx/aas/adapter/http.py | 49 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 2d862ab70..c4c41ba54 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -442,17 +442,21 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_submodel_all), Rule("/", methods=["POST"], endpoint=self.post_submodel), Rule("/$metadata", methods=["GET"], endpoint=self.get_allsubmodels_metadata), + Rule("/$reference", methods=["GET"], endpoint=self.get_allsubmodels_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodels_reference), Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_elements_id_short_path), Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_metadata), + Rule("/$reference", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path), @@ -464,6 +468,8 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.delete_submodel_submodel_elements_id_short_path), Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), + Rule("/$reference", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path_reference), Submount("/constraints", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), @@ -710,6 +716,20 @@ def get_allsubmodels_metadata(self, request: Request, url_args: Dict, **_kwargs) submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) return response_t(list(submodels), stripped=True) + def get_allsubmodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + id_short = request.args.get("idShort") + if id_short is not None: + submodels = filter(lambda sm: sm.id_short == id_short, submodels) + semantic_id = request.args.get("semanticId") + if semantic_id is not None: + if semantic_id is not None: + spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + references: Iterator[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] + return response_t(list(references), stripped=is_stripped_request(request)) + # --------- SUBMODEL ROUTES --------- def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -730,6 +750,14 @@ def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> submodel.update() return response_t(submodel, stripped=True) + def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + reference = model.ModelReference.from_referable(submodel) + return response_t(reference, stripped=is_stripped_request(request)) + def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) @@ -744,7 +772,7 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kw response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - return response_t(list(submodel.submodel_element)) + return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec @@ -754,6 +782,16 @@ def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Di submodel.update() return response_t(list(submodel.submodel_element), stripped=True) + def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec + # TODO: support content, extent, semanticId parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + references: Iterator[model.ModelReference] = [model.ModelReference.from_referable(element) for element in + submodel.submodel_element] + return response_t(list(references), stripped=is_stripped_request(request)) + def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) @@ -771,6 +809,15 @@ def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(submodel_element, stripped=True) + def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters + response_t = get_response_type(request) + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + reference = model.ModelReference.from_referable(submodel_element) + return response_t(reference, stripped=is_stripped_request(request)) + def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) From ac406f53a6dc386466f952719a70f52c7cc5b238 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 17 Mar 2024 20:43:35 +0100 Subject: [PATCH 113/157] adapter.http: fix line length --- basyx/aas/adapter/http.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index c4c41ba54..d1eeac0e7 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -727,7 +727,8 @@ def get_allsubmodels_reference(self, request: Request, url_args: Dict, **_kwargs if semantic_id is not None: spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) - references: Iterator[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] + references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) + for submodel in submodels] return response_t(list(references), stripped=is_stripped_request(request)) # --------- SUBMODEL ROUTES --------- @@ -788,8 +789,8 @@ def get_submodel_submodel_elements_reference(self, request: Request, url_args: D response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() - references: Iterator[model.ModelReference] = [model.ModelReference.from_referable(element) for element in - submodel.submodel_element] + references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in + submodel.submodel_element] return response_t(list(references), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -809,7 +810,8 @@ def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return response_t(submodel_element, stripped=True) - def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs)\ + -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) From 8dbba429f0d2c4ba4587cf637b4f7e7efe81ebd3 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Mon, 18 Mar 2024 11:57:23 +0100 Subject: [PATCH 114/157] adapter.http: refactoring submodel repo routes --- basyx/aas/adapter/http.py | 98 +++++++++++++++------------------------ 1 file changed, 38 insertions(+), 60 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d1eeac0e7..47432cce1 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -678,8 +678,7 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** raise NotFound(f"The AAS {aas!r} doesn't have the reference {url_args['submodel_ref']!r}!") # ------ SUBMODEL REPO ROUTES ------- - def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def _get_submodels_python(self, request: Request, url_args: Dict) -> Iterator[model.Submodel]: submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) id_short = request.args.get("idShort") if id_short is not None: @@ -689,6 +688,11 @@ def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Respo if semantic_id is not None: spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + return submodels + + def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodels = self._get_submodels_python(request, url_args) return response_t(list(submodels), stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: @@ -706,27 +710,12 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte def get_allsubmodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) - id_short = request.args.get("idShort") - if id_short is not None: - submodels = filter(lambda sm: sm.id_short == id_short, submodels) - semantic_id = request.args.get("semanticId") - if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) - submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + submodels = self._get_submodels_python(request, url_args) return response_t(list(submodels), stripped=True) def get_allsubmodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) - id_short = request.args.get("idShort") - if id_short is not None: - submodels = filter(lambda sm: sm.id_short == id_short, submodels) - semantic_id = request.args.get("semanticId") - if semantic_id is not None: - if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) - submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + submodels = self._get_submodels_python(request, url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] return response_t(list(references), stripped=is_stripped_request(request)) @@ -738,31 +727,33 @@ def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Respon self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) return response_t() - def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def _get_submodel_python(self, url_args: Dict) -> model.Submodel: # TODO: support content, extent parameters - response_t = get_response_type(request) submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() + return submodel + + def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + # TODO: support content, extent parameters + response_t = get_response_type(request) + submodel = self._get_submodel_python(url_args) return response_t(submodel, stripped=is_stripped_request(request)) def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) return response_t(submodel, stripped=True) def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) reference = model.ModelReference.from_referable(submodel) return response_t(reference, stripped=is_stripped_request(request)) def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) submodel.update_from(HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request))) submodel.commit() return response_t() @@ -771,61 +762,57 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kw # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) return response_t(list(submodel.submodel_element), stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in submodel.submodel_element] return response_t(list(references), stripped=is_stripped_request(request)) + def _get_submodel_submodel_elements_id_short_path_python(self, url_args: Dict) \ + -> model.SubmodelElement: + # TODO: support content, extent parameters + submodel = self._get_submodel_python(url_args) + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + return submodel_element + def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) return response_t(submodel_element, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) return response_t(submodel_element, stripped=True) def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs)\ -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) reference = model.ModelReference.from_referable(submodel_element) return response_t(reference, stripped=is_stripped_request(request)) def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_short_path = url_args.get("id_shorts", []) parent = self._get_submodel_or_nested_submodel_element(submodel, id_short_path) if not isinstance(parent, model.UniqueIdShortNamespace): @@ -848,10 +835,7 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameter response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, # type: ignore @@ -863,8 +847,7 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_short_path: List[str] = url_args["id_shorts"] parent: model.UniqueIdShortNamespace = self._expect_namespace( self._get_submodel_or_nested_submodel_element(submodel, id_short_path[:-1]), @@ -876,8 +859,7 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: @@ -891,8 +873,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) @@ -911,8 +892,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) @@ -942,9 +922,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - submodel = self._get_obj_ts(submodel_identifier, model.Submodel) - submodel.update() + submodel = self._get_submodel_python(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier_type = url_args["qualifier_type"] From 857b3dfd77ed00ab5971e8caaa86b8c195eb5c7a Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Mon, 18 Mar 2024 12:51:51 +0100 Subject: [PATCH 115/157] adapter.http: fixing post submodelelement route --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 47432cce1..f4a27d9d4 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -823,7 +823,7 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar is_stripped_request(request)) try: parent.add_referable(new_submodel_element) - except KeyError: + except model.AASConstraintViolation: raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " f"within {parent}!") created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { From ffb833b81eaaa4dc0f9d1973da4ac493d5df1b1e Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Mon, 18 Mar 2024 22:02:17 +0100 Subject: [PATCH 116/157] adapter.http: implement the recommended changes --- basyx/aas/adapter/http.py | 174 ++++++++++++++++++-------------------- 1 file changed, 80 insertions(+), 94 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index f4a27d9d4..9a1beb731 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -143,7 +143,7 @@ def serialize(self, obj: ResponseData, stripped: bool) -> str: response_elem = object_to_xml_element(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) - return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") + return etree.tostring(response_elem, xml_declaration=True, encoding=ENCODING) class XmlResponseAlt(XmlResponse): @@ -218,6 +218,22 @@ def is_stripped_request(request: Request) -> bool: T = TypeVar("T") +ENCODING = "utf-8" + + +def base64url_decode(data: str) -> str: + try: + # If the requester omits the base64 padding, an exception will be raised. + # However, Python doesn't complain about too much padding, + # thus we simply always append two padding characters (==). + # See also: https://stackoverflow.com/a/49459036/4780052 + decoded = base64.urlsafe_b64decode(data + "==").decode(ENCODING) + except binascii.Error: + raise BadRequest(f"Encoded data {str(data)} is invalid base64url!") + except UnicodeDecodeError: + raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + return decoded + class HTTPApiDecoder: # these are the types we can construct (well, only the ones we need) @@ -232,8 +248,6 @@ class HTTPApiDecoder: model.Reference: XMLConstructables.REFERENCE } - encoding = "utf-8" - @classmethod def check_type_supportance(cls, type_: type): if type_ not in cls.type_constructables_map: @@ -245,18 +259,6 @@ def assert_type(cls, obj: object, type_: Type[T]) -> T: raise UnprocessableEntity(f"Object {obj!r} is not of type {type_.__name__}!") return obj - @classmethod - def base64_decode(cls, data: Union[str, bytes]) -> str: - try: - # If the requester omits the base64 padding, an exception will be raised. - # However, Python doesn't complain about too much padding, - # thus we simply always append two padding characters (==). - # See also: https://stackoverflow.com/a/49459036/4780052 - decoded = base64.urlsafe_b64decode(data + "==").decode(cls.encoding) - except binascii.Error: - raise BadRequest(f"Encoded data {str(data)} is invalid base64url!") - return decoded - @classmethod def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool) -> List[T]: cls.check_type_supportance(expect_type) @@ -284,7 +286,7 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool elif expect_type is model.SpecificAssetId: constructor = decoder._construct_specific_asset_id # type: ignore[assignment] elif expect_type is model.Reference: - constructor = decoder._construct_model_reference # type: ignore[assignment] + constructor = decoder._construct_reference # type: ignore[assignment] args.append(model.Submodel) if constructor is not None: @@ -297,9 +299,9 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool return [cls.assert_type(obj, expect_type) for obj in parsed] @classmethod - def base64json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool)\ + def base64urljson_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool)\ -> List[T]: - data = cls.base64_decode(data) + data = base64url_decode(data) return cls.json_list(data, expect_type, stripped, expect_single) @classmethod @@ -307,8 +309,8 @@ def json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> return cls.json_list(data, expect_type, stripped, True)[0] @classmethod - def base64json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: - data = cls.base64_decode(data) + def base64urljson(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: + data = base64url_decode(data) return cls.json_list(data, expect_type, stripped, True)[0] @classmethod @@ -351,7 +353,6 @@ def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> class Base64UrlJsonConverter(werkzeug.routing.UnicodeConverter): - encoding = "utf-8" def __init__(self, url_map, t: str): super().__init__(url_map) @@ -362,17 +363,11 @@ def __init__(self, url_map, t: str): raise ValueError(f"invalid value t={t}") def to_url(self, value: object) -> str: - return super().to_url(base64.urlsafe_b64encode(json.dumps(value, cls=AASToJsonEncoder).encode(self.encoding))) + return super().to_url(base64.urlsafe_b64encode(json.dumps(value, cls=AASToJsonEncoder).encode(ENCODING))) def to_python(self, value: str) -> object: value = super().to_python(value) - try: - decoded = base64.urlsafe_b64decode(super().to_python(value)).decode(self.encoding) - except binascii.Error: - raise BadRequest(f"Encoded json object {value} is invalid base64url!") - except UnicodeDecodeError: - raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") - + decoded = base64url_decode(super().to_python(value)) try: return HTTPApiDecoder.json(decoded, self.type, False) except json.JSONDecodeError: @@ -380,23 +375,13 @@ def to_python(self, value: str) -> object: class IdentifierConverter(werkzeug.routing.UnicodeConverter): - encoding = "utf-8" def to_url(self, value: model.Identifier) -> str: - return super().to_url(base64.urlsafe_b64encode(value.encode(self.encoding))) + return super().to_url(base64.urlsafe_b64encode(value.encode(ENCODING))) def to_python(self, value: str) -> model.Identifier: value = super().to_python(value) - try: - # If the requester omits the base64 padding, an exception will be raised. - # However, Python doesn't complain about too much padding, - # thus we simply always append two padding characters (==). - # See also: https://stackoverflow.com/a/49459036/4780052 - decoded = base64.urlsafe_b64decode(super().to_python(value) + "==").decode(self.encoding) - except binascii.Error: - raise BadRequest(f"Encoded identifier {value} is invalid base64url!") - except UnicodeDecodeError: - raise BadRequest(f"Encoded base64url value is not a valid {self.encoding} string!") + decoded = base64url_decode(super().to_python(value)) return decoded @@ -441,8 +426,8 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_all), Rule("/", methods=["POST"], endpoint=self.post_submodel), - Rule("/$metadata", methods=["GET"], endpoint=self.get_allsubmodels_metadata), - Rule("/$reference", methods=["GET"], endpoint=self.get_allsubmodels_reference), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_all_metadata), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_all_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), @@ -565,6 +550,30 @@ def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, except KeyError as e: raise NotFound(f"Submodel element with id_short {arg} not found in {namespace!r}") from e + def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + id_short = request.args.get("idShort") + if id_short is not None: + submodels = filter(lambda sm: sm.id_short == id_short, submodels) + semantic_id = request.args.get("semanticId") + if semantic_id is not None: + spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + return submodels + + def _get_submodel(self, url_args: Dict) -> model.Submodel: + # TODO: support content, extent parameters + submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) + submodel.update() + return submodel + + def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ + -> model.SubmodelElement: + # TODO: support content, extent parameters + submodel = self._get_submodel(url_args) + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) + return submodel_element + def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: @@ -594,7 +603,7 @@ def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: aas = filter(lambda shell: shell.id_short == id_short, aas) asset_ids = request.args.get("assetIds") if asset_ids is not None: - spec_asset_ids = HTTPApiDecoder.base64json_list(asset_ids, model.SpecificAssetId, False, False) + spec_asset_ids = HTTPApiDecoder.base64urljson_list(asset_ids, model.SpecificAssetId, False, False) # TODO: it's currently unclear how to filter with these SpecificAssetIds return response_t(list(aas)) @@ -678,21 +687,9 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** raise NotFound(f"The AAS {aas!r} doesn't have the reference {url_args['submodel_ref']!r}!") # ------ SUBMODEL REPO ROUTES ------- - def _get_submodels_python(self, request: Request, url_args: Dict) -> Iterator[model.Submodel]: - submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) - id_short = request.args.get("idShort") - if id_short is not None: - submodels = filter(lambda sm: sm.id_short == id_short, submodels) - semantic_id = request.args.get("semanticId") - if semantic_id is not None: - if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64json(semantic_id, model.Reference, False) - submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) - return submodels - def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels_python(request, url_args) + submodels = self._get_submodels(request) return response_t(list(submodels), stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: @@ -708,17 +705,17 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte }, force_external=True) return response_t(submodel, status=201, headers={"Location": created_resource_url}) - def get_allsubmodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_submodel_all_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels_python(request, url_args) + submodels = self._get_submodels(request) return response_t(list(submodels), stripped=True) - def get_allsubmodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_submodel_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels_python(request, url_args) + submodels = self._get_submodels(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] - return response_t(list(references), stripped=is_stripped_request(request)) + return response_t(references, stripped=is_stripped_request(request)) # --------- SUBMODEL ROUTES --------- @@ -727,33 +724,27 @@ def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Respon self.object_store.remove(self._get_obj_ts(url_args["submodel_id"], model.Submodel)) return response_t() - def _get_submodel_python(self, url_args: Dict) -> model.Submodel: - # TODO: support content, extent parameters - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - return submodel - def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) return response_t(submodel, stripped=is_stripped_request(request)) def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) return response_t(submodel, stripped=True) def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) reference = model.ModelReference.from_referable(submodel) return response_t(reference, stripped=is_stripped_request(request)) def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) submodel.update_from(HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request))) submodel.commit() return response_t() @@ -762,57 +753,50 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kw # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) return response_t(list(submodel.submodel_element), stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in submodel.submodel_element] - return response_t(list(references), stripped=is_stripped_request(request)) - - def _get_submodel_submodel_elements_id_short_path_python(self, url_args: Dict) \ - -> model.SubmodelElement: - # TODO: support content, extent parameters - submodel = self._get_submodel_python(url_args) - submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) - return submodel_element + return response_t(references, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) return response_t(submodel_element, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) return response_t(submodel_element, stripped=True) def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs)\ -> Response: # TODO: support content, extent parameters response_t = get_response_type(request) - submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) reference = model.ModelReference.from_referable(submodel_element) return response_t(reference, stripped=is_stripped_request(request)) def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): # TODO: support content, extent parameter response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_short_path = url_args.get("id_shorts", []) parent = self._get_submodel_or_nested_submodel_element(submodel, id_short_path) if not isinstance(parent, model.UniqueIdShortNamespace): @@ -823,7 +807,9 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar is_stripped_request(request)) try: parent.add_referable(new_submodel_element) - except model.AASConstraintViolation: + except model.AASConstraintViolation as e: + if e.constraint_id != 22: + raise raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " f"within {parent}!") created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { @@ -835,7 +821,7 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content, extent parameter response_t = get_response_type(request) - submodel_element = self._get_submodel_submodel_elements_id_short_path_python(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, # type: ignore @@ -847,7 +833,7 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_short_path: List[str] = url_args["id_shorts"] parent: model.UniqueIdShortNamespace = self._expect_namespace( self._get_submodel_or_nested_submodel_element(submodel, id_short_path[:-1]), @@ -859,7 +845,7 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: @@ -873,7 +859,7 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) @@ -892,7 +878,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) @@ -922,7 +908,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) - submodel = self._get_submodel_python(url_args) + submodel = self._get_submodel(url_args) id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier_type = url_args["qualifier_type"] From 78d6af9aacc1dc154edfdf082c31eb7b67713b76 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Wed, 20 Mar 2024 11:20:44 +0100 Subject: [PATCH 117/157] adapter.http: implement the new recommended changes --- basyx/aas/adapter/http.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 9a1beb731..7a960c8a3 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -143,7 +143,7 @@ def serialize(self, obj: ResponseData, stripped: bool) -> str: response_elem = object_to_xml_element(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) - return etree.tostring(response_elem, xml_declaration=True, encoding=ENCODING) + return etree.tostring(response_elem, xml_declaration=True, encoding=BASE64URL_ENCODING) class XmlResponseAlt(XmlResponse): @@ -218,7 +218,7 @@ def is_stripped_request(request: Request) -> bool: T = TypeVar("T") -ENCODING = "utf-8" +BASE64URL_ENCODING = "utf-8" def base64url_decode(data: str) -> str: @@ -227,14 +227,19 @@ def base64url_decode(data: str) -> str: # However, Python doesn't complain about too much padding, # thus we simply always append two padding characters (==). # See also: https://stackoverflow.com/a/49459036/4780052 - decoded = base64.urlsafe_b64decode(data + "==").decode(ENCODING) + decoded = base64.urlsafe_b64decode(data + "==").decode(BASE64URL_ENCODING) except binascii.Error: - raise BadRequest(f"Encoded data {str(data)} is invalid base64url!") + raise BadRequest(f"Encoded data {data} is invalid base64url!") except UnicodeDecodeError: - raise BadRequest(f"Encoded base64url value is not a valid utf-8 string!") + raise BadRequest(f"Encoded base64url value is not a valid {BASE64URL_ENCODING} string!") return decoded +def base64url_encode(data: str) -> bytes: + encoded = base64.urlsafe_b64encode(data.encode(BASE64URL_ENCODING)) + return encoded + + class HTTPApiDecoder: # these are the types we can construct (well, only the ones we need) type_constructables_map = { @@ -287,7 +292,6 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool constructor = decoder._construct_specific_asset_id # type: ignore[assignment] elif expect_type is model.Reference: constructor = decoder._construct_reference # type: ignore[assignment] - args.append(model.Submodel) if constructor is not None: # construct elements that aren't self-identified @@ -299,7 +303,7 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool return [cls.assert_type(obj, expect_type) for obj in parsed] @classmethod - def base64urljson_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool, expect_single: bool)\ + def base64urljson_list(cls, data: str, expect_type: Type[T], stripped: bool, expect_single: bool)\ -> List[T]: data = base64url_decode(data) return cls.json_list(data, expect_type, stripped, expect_single) @@ -309,7 +313,7 @@ def json(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> return cls.json_list(data, expect_type, stripped, True)[0] @classmethod - def base64urljson(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool) -> T: + def base64urljson(cls, data: str, expect_type: Type[T], stripped: bool) -> T: data = base64url_decode(data) return cls.json_list(data, expect_type, stripped, True)[0] @@ -363,7 +367,7 @@ def __init__(self, url_map, t: str): raise ValueError(f"invalid value t={t}") def to_url(self, value: object) -> str: - return super().to_url(base64.urlsafe_b64encode(json.dumps(value, cls=AASToJsonEncoder).encode(ENCODING))) + return super().to_url(base64url_encode(json.dumps(value, cls=AASToJsonEncoder))) def to_python(self, value: str) -> object: value = super().to_python(value) @@ -377,7 +381,7 @@ def to_python(self, value: str) -> object: class IdentifierConverter(werkzeug.routing.UnicodeConverter): def to_url(self, value: model.Identifier) -> str: - return super().to_url(base64.urlsafe_b64encode(value.encode(ENCODING))) + return super().to_url(base64url_encode(value)) def to_python(self, value: str) -> model.Identifier: value = super().to_python(value) @@ -557,19 +561,18 @@ def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: submodels = filter(lambda sm: sm.id_short == id_short, submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) + spec_semantic_id = (HTTPApiDecoder.base64urljson + (semantic_id, model.Reference, False)) # type: ignore[type-abstract] submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) return submodels def _get_submodel(self, url_args: Dict) -> model.Submodel: - # TODO: support content, extent parameters submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) submodel.update() return submodel def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: - # TODO: support content, extent parameters submodel = self._get_submodel(url_args) submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return submodel_element @@ -725,7 +728,6 @@ def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Respon return response_t() def get_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) return response_t(submodel, stripped=is_stripped_request(request)) @@ -736,7 +738,6 @@ def get_submodels_metadata(self, request: Request, url_args: Dict, **_kwargs) -> return response_t(submodel, stripped=True) def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) reference = model.ModelReference.from_referable(submodel) @@ -773,28 +774,24 @@ def get_submodel_submodel_elements_reference(self, request: Request, url_args: D return response_t(references, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) return response_t(submodel_element, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) return response_t(submodel_element, stripped=True) def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, **_kwargs)\ -> Response: - # TODO: support content, extent parameters response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) reference = model.ModelReference.from_referable(submodel_element) return response_t(reference, stripped=is_stripped_request(request)) def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, map_adapter: MapAdapter): - # TODO: support content, extent parameter response_t = get_response_type(request) submodel = self._get_submodel(url_args) id_short_path = url_args.get("id_shorts", []) @@ -819,7 +816,6 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar return response_t(new_submodel_element, status=201, headers={"Location": created_resource_url}) def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content, extent parameter response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] From b2f9d7988159c0f041e7e7d52031eeab980f1c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 16:01:18 +0100 Subject: [PATCH 118/157] adapter.http: hardcode `utf-8` for XML serialization --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7a960c8a3..7095d13a2 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -143,7 +143,7 @@ def serialize(self, obj: ResponseData, stripped: bool) -> str: response_elem = object_to_xml_element(obj) parent.append(response_elem) etree.cleanup_namespaces(parent) - return etree.tostring(response_elem, xml_declaration=True, encoding=BASE64URL_ENCODING) + return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") class XmlResponseAlt(XmlResponse): From c2c6cc1e12fb56752520998030f220819af2ace4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 17:31:55 +0100 Subject: [PATCH 119/157] adapter.http: update AAS submodel refs path --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7095d13a2..20c54f651 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -419,7 +419,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["DELETE"], endpoint=self.delete_aas), Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), - Submount("/submodels", [ + Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), Rule("//", methods=["DELETE"], From 30e739d688a0f147ed575626335a4b7ba77c9ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 17:33:39 +0100 Subject: [PATCH 120/157] adapter.http: update AAS submodel refs `DELETE` route The route now uses the submodel identifier instead of the submodel reference. --- basyx/aas/adapter/http.py | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 20c54f651..fd66c3935 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -356,28 +356,6 @@ def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> return cls.xml(request.get_data(), expect_type, stripped) -class Base64UrlJsonConverter(werkzeug.routing.UnicodeConverter): - - def __init__(self, url_map, t: str): - super().__init__(url_map) - self.type: type - if t == "ModelReference": - self.type = model.ModelReference - else: - raise ValueError(f"invalid value t={t}") - - def to_url(self, value: object) -> str: - return super().to_url(base64url_encode(json.dumps(value, cls=AASToJsonEncoder))) - - def to_python(self, value: str) -> object: - value = super().to_python(value) - decoded = base64url_decode(super().to_python(value)) - try: - return HTTPApiDecoder.json(decoded, self.type, False) - except json.JSONDecodeError: - raise BadRequest(f"{decoded} is not a valid json string!") - - class IdentifierConverter(werkzeug.routing.UnicodeConverter): def to_url(self, value: model.Identifier) -> str: @@ -422,7 +400,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]) ]) @@ -489,8 +467,7 @@ def __init__(self, object_store: model.AbstractObjectStore): ]) ], converters={ "identifier": IdentifierConverter, - "id_short_path": IdShortPathConverter, - "base64url_json": Base64UrlJsonConverter + "id_short_path": IdShortPathConverter }) # TODO: the parameters can be typed via builtin wsgiref with Python 3.11+ @@ -683,11 +660,11 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() for sm_ref in aas.submodel: - if sm_ref == url_args["submodel_ref"]: + if sm_ref.get_identifier() == url_args["submodel_id"]: aas.submodel.remove(sm_ref) aas.commit() return response_t() - raise NotFound(f"The AAS {aas!r} doesn't have the reference {url_args['submodel_ref']!r}!") + raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {url_args['submodel_id']!r}!") # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: From 990b7f91b1133fa056be74fb3d635ae147f47abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 22:00:55 +0100 Subject: [PATCH 121/157] adapter.http: suffix submodel refs deletion route with a slash --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index fd66c3935..ca9ea8bf6 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -400,7 +400,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("/", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]) ]) From b88db588a2823004d1b8e683adb85f007dcacc24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 22:02:32 +0100 Subject: [PATCH 122/157] adapter.http: refactor submodel ref access as separate function --- basyx/aas/adapter/http.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index ca9ea8bf6..2a95ebda3 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -531,6 +531,16 @@ def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, except KeyError as e: raise NotFound(f"Submodel element with id_short {arg} not found in {namespace!r}") from e + @classmethod + def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_id: model.NameType) \ + -> model.ModelReference[model.Submodel]: + # TODO: this is currently O(n), could be O(1) as aas.submodel, but keys would have to precisely match, as they + # are hashed including their KeyType + for ref in aas.submodel: + if ref.get_identifier() == submodel_id: + return ref + raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") + def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) id_short = request.args.get("idShort") @@ -659,12 +669,9 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() - for sm_ref in aas.submodel: - if sm_ref.get_identifier() == url_args["submodel_id"]: - aas.submodel.remove(sm_ref) - aas.commit() - return response_t() - raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {url_args['submodel_id']!r}!") + aas.submodel.remove(self._get_submodel_reference(aas, url_args["submodel_id"])) + aas.commit() + return response_t() # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: From 9c9cd44730ffae9351e244b6ef02ec1c5bc1367c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 22:23:40 +0100 Subject: [PATCH 123/157] adapter.http: suffix slashes to all routes --- basyx/aas/adapter/http.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7095d13a2..45234f536 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -430,21 +430,21 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodels", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_all), Rule("/", methods=["POST"], endpoint=self.post_submodel), - Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_all_metadata), - Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_all_reference), + Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_all_metadata), + Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_all_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), - Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), - Rule("/$reference", methods=["GET"], endpoint=self.get_submodels_reference), + Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodels_metadata), + Rule("/$reference/", methods=["GET"], endpoint=self.get_submodels_reference), Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/$metadata", methods=["GET"], + Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_metadata), - Rule("/$reference", methods=["GET"], + Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_reference), Submount("/", [ Rule("/", methods=["GET"], @@ -455,9 +455,9 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.put_submodel_submodel_elements_id_short_path), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_elements_id_short_path), - Rule("/$metadata", methods=["GET"], + Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), - Rule("/$reference", methods=["GET"], + Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), Submount("/constraints", [ Rule("/", methods=["GET"], From bae8fbcacb68a5f7a0f3ab162fc73fe5e62885c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 20 Mar 2024 22:29:22 +0100 Subject: [PATCH 124/157] adapter.http: change `base64url_encode()` function to return `str` --- basyx/aas/adapter/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 7095d13a2..5ca74971f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -235,8 +235,8 @@ def base64url_decode(data: str) -> str: return decoded -def base64url_encode(data: str) -> bytes: - encoded = base64.urlsafe_b64encode(data.encode(BASE64URL_ENCODING)) +def base64url_encode(data: str) -> str: + encoded = base64.urlsafe_b64encode(data.encode(BASE64URL_ENCODING)).decode("ascii") return encoded From c86283ab48fdf96e37256d1b4932add9a3d90066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 28 Mar 2024 17:12:36 +0100 Subject: [PATCH 125/157] adapter.http: implement AAS API submodel routes via redirects The submodel routes of the AAS API `/shells//submodels` are the same already implemented as the submodel API, so we return a redirect here, after checking that the AAS indeed has a reference to the requested submodel. `PUT` and `DELETE` are different, as we also have to update the AAS in this case, either by adding a new updated reference in case of a `PUT` request changes the submodel id, or by removing the submodel reference for `DELETE` requests. --- basyx/aas/adapter/http.py | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 556aaca46..ed617218d 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -21,6 +21,7 @@ import werkzeug.exceptions import werkzeug.routing import werkzeug.urls +import werkzeug.utils from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response @@ -402,6 +403,12 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) + ]), + Submount("/submodels/", [ + Rule("/", methods=["PUT"], endpoint=self.put_aas_submodel_refs_submodel), + Rule("/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_submodel), + Rule("/", endpoint=self.aas_submodel_refs_redirect), + Rule("//", endpoint=self.aas_submodel_refs_redirect) ]) ]) ]), @@ -673,6 +680,49 @@ def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, ** aas.commit() return response_t() + def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) + submodel = self._resolve_reference(sm_ref) + new_submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) + # determine whether the id changed in advance, in case something goes wrong while updating the submodel + id_changed: bool = submodel.id != new_submodel.id + # TODO: https://github.com/eclipse-basyx/basyx-python-sdk/issues/216 + submodel.update_from(new_submodel) + submodel.commit() + if id_changed: + aas.submodel.remove(sm_ref) + aas.submodel.add(model.ModelReference.from_referable(submodel)) + aas.commit() + return response_t() + + def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) + submodel = self._resolve_reference(sm_ref) + self.object_store.remove(submodel) + aas.submodel.remove(sm_ref) + aas.commit() + return response_t() + + def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + aas.update() + # the following makes sure the reference exists + self._get_submodel_reference(aas, url_args["submodel_id"]) + redirect_url = map_adapter.build(self.get_submodel, { + "submodel_id": url_args["submodel_id"] + }, force_external=True) + if "path" in url_args: + redirect_url += url_args["path"] + "/" + if request.query_string: + redirect_url += "?" + request.query_string.decode("ascii") + return werkzeug.utils.redirect(redirect_url, 307) + # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) From 7fad3f24f5084ca3acdaf178f4864a989c141985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 28 Mar 2024 17:33:10 +0100 Subject: [PATCH 126/157] adapter.http: move `asset-information` routes to a submount --- basyx/aas/adapter/http.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 556aaca46..b7fe99a78 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -395,8 +395,10 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/", methods=["DELETE"], endpoint=self.delete_aas), - Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), + Submount("/asset-information", [ + Rule("/", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/", methods=["PUT"], endpoint=self.put_aas_asset_information), + ]), Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), From 88bad21c985f6f1ed0b753b043edbdb59c9ff065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 29 Mar 2024 17:16:40 +0100 Subject: [PATCH 127/157] adapter.http: rename `/constraints` routes to `/qualifiers` --- basyx/aas/adapter/http.py | 48 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 556aaca46..20159fb02 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -437,30 +437,30 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), - Submount("/constraints", [ + Submount("/qualifiers", [ Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), + endpoint=self.get_submodel_submodel_element_qualifiers), Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), + endpoint=self.post_submodel_submodel_element_qualifiers), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_qualifiers), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_qualifiers), ]) ]), ]), - Submount("/constraints", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_constraints), + Submount("/qualifiers", [ + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_constraints), - Rule("//", methods=["GET"], - endpoint=self.get_submodel_submodel_element_constraints), - Rule("//", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_constraints), - Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_constraints), + endpoint=self.post_submodel_submodel_element_qualifiers), + Rule("//", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("//", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_qualifiers), + Rule("//", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_qualifiers), ]) ]) ]) @@ -822,7 +822,7 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) return response_t() - def get_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ + def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) submodel = self._get_submodel(url_args) @@ -835,7 +835,7 @@ def get_submodel_submodel_element_constraints(self, request: Request, url_args: except KeyError: raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") - def post_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ + def post_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] @@ -847,14 +847,14 @@ def post_submodel_submodel_element_constraints(self, request: Request, url_args: raise Conflict(f"Qualifier with type {qualifier.type} already exists!") sm_or_se.qualifier.add(qualifier) sm_or_se.commit() - created_resource_url = map_adapter.build(self.get_submodel_submodel_element_constraints, { + created_resource_url = map_adapter.build(self.get_submodel_submodel_element_qualifiers, { "submodel_id": submodel_identifier, "id_shorts": id_shorts if len(id_shorts) != 0 else None, "qualifier_type": qualifier.type }, force_external=True) return response_t(qualifier, status=201, headers={"Location": created_resource_url}) - def put_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ + def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ -> Response: response_t = get_response_type(request) submodel_identifier = url_args["submodel_id"] @@ -877,7 +877,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: sm_or_se.qualifier.add(new_qualifier) sm_or_se.commit() if qualifier_type_changed: - created_resource_url = map_adapter.build(self.get_submodel_submodel_element_constraints, { + created_resource_url = map_adapter.build(self.get_submodel_submodel_element_qualifiers, { "submodel_id": submodel_identifier, "id_shorts": id_shorts if len(id_shorts) != 0 else None, "qualifier_type": new_qualifier.type @@ -885,7 +885,7 @@ def put_submodel_submodel_element_constraints(self, request: Request, url_args: return response_t(new_qualifier, status=201, headers={"Location": created_resource_url}) return response_t(new_qualifier) - def delete_submodel_submodel_element_constraints(self, request: Request, url_args: Dict, **_kwargs) \ + def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) submodel = self._get_submodel(url_args) From 3c4a9f261184b15fce0f4068552861725ced3c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 29 Mar 2024 17:21:03 +0100 Subject: [PATCH 128/157] adapter.http: fix `Qualifier` JSON deserialization --- basyx/aas/adapter/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 20159fb02..b23509a65 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -292,6 +292,8 @@ def json_list(cls, data: Union[str, bytes], expect_type: Type[T], stripped: bool constructor = decoder._construct_specific_asset_id # type: ignore[assignment] elif expect_type is model.Reference: constructor = decoder._construct_reference # type: ignore[assignment] + elif expect_type is model.Qualifier: + constructor = decoder._construct_qualifier # type: ignore[assignment] if constructor is not None: # construct elements that aren't self-identified From b11f672b0ba0e6c0abe16465942a938d6a5f6326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 29 Mar 2024 17:21:58 +0100 Subject: [PATCH 129/157] adapter.http: refactor qualifier retrieval/removal --- basyx/aas/adapter/http.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index b23509a65..0daff2650 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -533,6 +533,13 @@ def _namespace_submodel_element_op(cls, namespace: model.UniqueIdShortNamespace, except KeyError as e: raise NotFound(f"Submodel element with id_short {arg} not found in {namespace!r}") from e + @classmethod + def _qualifiable_qualifier_op(cls, qualifiable: model.Qualifiable, op: Callable[[str], T], arg: str) -> T: + try: + return op(arg) + except KeyError as e: + raise NotFound(f"Qualifier with type {arg!r} not found in {qualifiable!r}") from e + @classmethod def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_id: model.NameType) \ -> model.ModelReference[model.Submodel]: @@ -832,10 +839,7 @@ def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: D qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: return response_t(list(sm_or_se.qualifier)) - try: - return response_t(sm_or_se.get_qualifier_by_type(qualifier_type)) - except KeyError: - raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + return response_t(self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type)) def post_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, map_adapter: MapAdapter) \ -> Response: @@ -865,13 +869,7 @@ def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: D sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) qualifier_type = url_args["qualifier_type"] - try: - qualifier = sm_or_se.get_qualifier_by_type(qualifier_type) - except KeyError: - raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") - if type(qualifier) is not type(new_qualifier): - raise UnprocessableEntity(f"Type of new qualifier {new_qualifier} doesn't not match " - f"the current submodel element {qualifier}") + qualifier = self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type) qualifier_type_changed = qualifier_type != new_qualifier.type if qualifier_type_changed and sm_or_se.qualifier.contains_id("type", new_qualifier.type): raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") @@ -894,10 +892,7 @@ def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args id_shorts: List[str] = url_args.get("id_shorts", []) sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) qualifier_type = url_args["qualifier_type"] - try: - sm_or_se.remove_qualifier_by_type(qualifier_type) - except KeyError: - raise NotFound(f"No constraint with type {qualifier_type} found in {sm_or_se}") + self._qualifiable_qualifier_op(sm_or_se, sm_or_se.remove_qualifier_by_type, qualifier_type) sm_or_se.commit() return response_t() From 5e91f4d91027820a67af38fce928384b48afdf6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 29 Mar 2024 17:23:44 +0100 Subject: [PATCH 130/157] adapter.http: improve an error mesage --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 0daff2650..18734de53 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -872,7 +872,7 @@ def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: D qualifier = self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type) qualifier_type_changed = qualifier_type != new_qualifier.type if qualifier_type_changed and sm_or_se.qualifier.contains_id("type", new_qualifier.type): - raise Conflict(f"A qualifier of type {new_qualifier.type} already exists for {sm_or_se}") + raise Conflict(f"A qualifier of type {new_qualifier.type!r} already exists for {sm_or_se!r}") sm_or_se.remove_qualifier_by_type(qualifier.type) sm_or_se.qualifier.add(new_qualifier) sm_or_se.commit() From c27e47fc92959a04d89996cfca1426436bd0bf83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sun, 31 Mar 2024 13:53:24 +0200 Subject: [PATCH 131/157] adapter.http: rename `IdentifierConverter` to `Base64URLConverter` The `IdentifierConverter` is also used to decode values from URLs, that aren't necessarily Identifiers, e.g. Qualifier types. Thus, a name like `Base64URLConverter` suits its use better and is also more expressive. For the same reasons, the key `identifier`, which was used for the `IdentifierConverter`, is renamed to `base64url`. --- basyx/aas/adapter/http.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 228921458..ea36b0681 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -359,7 +359,7 @@ def request_body(cls, request: Request, expect_type: Type[T], stripped: bool) -> return cls.xml(request.get_data(), expect_type, stripped) -class IdentifierConverter(werkzeug.routing.UnicodeConverter): +class Base64URLConverter(werkzeug.routing.UnicodeConverter): def to_url(self, value: model.Identifier) -> str: return super().to_url(base64url_encode(value)) @@ -394,7 +394,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), Rule("/", methods=["POST"], endpoint=self.post_aas), - Submount("/", [ + Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/", methods=["DELETE"], endpoint=self.delete_aas), @@ -405,10 +405,10 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_specific) ]), - Submount("/submodels/", [ + Submount("/submodels/", [ Rule("/", methods=["PUT"], endpoint=self.put_aas_submodel_refs_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_submodel), Rule("/", endpoint=self.aas_submodel_refs_redirect), @@ -421,7 +421,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["POST"], endpoint=self.post_submodel), Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_all_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_all_reference), - Submount("/", [ + Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), @@ -453,11 +453,11 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_submodel_submodel_element_qualifiers), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("//", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers), ]) ]), @@ -466,18 +466,18 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), Rule("/", methods=["POST"], endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("//", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("//", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers), ]) ]) ]) ]) ], converters={ - "identifier": IdentifierConverter, + "base64url": Base64URLConverter, "id_short_path": IdShortPathConverter }) From 973f81ac057ded7b093275ddd12483f6c95e76c5 Mon Sep 17 00:00:00 2001 From: Hadi Jannat Date: Wed, 20 Mar 2024 10:40:35 +0100 Subject: [PATCH 132/157] adapter.http: add `SpecificAssetId` filtering to `get_aas_all()` --- basyx/aas/adapter/http.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index ea36b0681..ee4dd4ed1 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -604,16 +604,34 @@ def handle_request(self, request: Request): # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def asset_id_matches(spec_asset_id, specific_asset_ids): + """Checks if a specific asset ID matches any within a list.""" + return any( + spec_asset_id == asset_id + for asset_id in specific_asset_ids + ) + response_t = get_response_type(request) - aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) + aas_iterable: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type( + model.AssetAdministrationShell) + + # Filter by 'idShort' if provided in the request id_short = request.args.get("idShort") if id_short is not None: - aas = filter(lambda shell: shell.id_short == id_short, aas) - asset_ids = request.args.get("assetIds") + aas_iterable = filter(lambda shell: shell.id_short == id_short, aas_iterable) + + # Filtering by base64url encoded SpecificAssetIds if provided + asset_ids = request.args.getlist("assetIds") if asset_ids is not None: - spec_asset_ids = HTTPApiDecoder.base64urljson_list(asset_ids, model.SpecificAssetId, False, False) - # TODO: it's currently unclear how to filter with these SpecificAssetIds - return response_t(list(aas)) + # Decode and instantiate SpecificAssetIds + spec_asset_ids = map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, + False), asset_ids) + # Filter AAS based on these SpecificAssetIds + aas_iterable = filter(lambda shell: all( + asset_id_matches(spec_asset_id, shell.asset_information.specific_asset_id) + for spec_asset_id in spec_asset_ids), aas_iterable) + + return response_t(list(aas_iterable)) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) From 237aebedafba93cf00acc9fbd02f4fef1dc52f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Tue, 2 Apr 2024 14:20:20 +0200 Subject: [PATCH 133/157] adapter.http: improve `SpecificAssetId` filtering --- basyx/aas/adapter/http.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index ee4dd4ed1..8790bd6cc 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -604,34 +604,24 @@ def handle_request(self, request: Request): # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: - def asset_id_matches(spec_asset_id, specific_asset_ids): - """Checks if a specific asset ID matches any within a list.""" - return any( - spec_asset_id == asset_id - for asset_id in specific_asset_ids - ) - response_t = get_response_type(request) - aas_iterable: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type( - model.AssetAdministrationShell) + aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) - # Filter by 'idShort' if provided in the request id_short = request.args.get("idShort") if id_short is not None: - aas_iterable = filter(lambda shell: shell.id_short == id_short, aas_iterable) + aas = filter(lambda shell: shell.id_short == id_short, aas) - # Filtering by base64url encoded SpecificAssetIds if provided asset_ids = request.args.getlist("assetIds") if asset_ids is not None: # Decode and instantiate SpecificAssetIds - spec_asset_ids = map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, - False), asset_ids) + # This needs to be a list, otherwise we can only iterate it once. + specific_asset_ids: List[model.SpecificAssetId] = list( + map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, False), asset_ids)) # Filter AAS based on these SpecificAssetIds - aas_iterable = filter(lambda shell: all( - asset_id_matches(spec_asset_id, shell.asset_information.specific_asset_id) - for spec_asset_id in spec_asset_ids), aas_iterable) + aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id + for specific_asset_id in specific_asset_ids), aas) - return response_t(list(aas_iterable)) + return response_t(list(aas)) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) From 74ea148989a0c3fae58feee89ac20c36e746a2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 3 Apr 2024 15:20:53 +0200 Subject: [PATCH 134/157] adapter.http: refactor AAS retrieval Methods `_get_shell()` and `_get_shells()` are added similarly to `_get_submodel()` and `_get_submodels()`, which were previously added in ffb833b81eaaa4dc0f9d1973da4ac493d5df1b1e. Furthermore, when requesting multiple AAS/Submodels, we're now also updating these in `_get_all_obj_of_type()` before returning them. Finally, updating AAS/Submodel objects is also moved to `_get_obj_ts()`. --- basyx/aas/adapter/http.py | 79 ++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 8790bd6cc..64fb1e1d1 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -490,11 +490,13 @@ def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._ identifiable = self.object_store.get(identifier) if not isinstance(identifiable, type_): raise NotFound(f"No {type_.__name__} with {identifier} found!") + identifiable.update() return identifiable def _get_all_obj_of_type(self, type_: Type[model.provider._IT]) -> Iterator[model.provider._IT]: for obj in self.object_store: if isinstance(obj, type_): + obj.update() yield obj def _resolve_reference(self, reference: model.ModelReference[model.base._RT]) -> model.base._RT: @@ -559,6 +561,28 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i return ref raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") + def _get_shells(self, request: Request) -> Iterator[model.AssetAdministrationShell]: + aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) + + id_short = request.args.get("idShort") + if id_short is not None: + aas = filter(lambda shell: shell.id_short == id_short, aas) + + asset_ids = request.args.getlist("assetIds") + if asset_ids is not None: + # Decode and instantiate SpecificAssetIds + # This needs to be a list, otherwise we can only iterate it once. + specific_asset_ids: List[model.SpecificAssetId] = list( + map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, False), asset_ids)) + # Filter AAS based on these SpecificAssetIds + aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id + for specific_asset_id in specific_asset_ids), aas) + + return aas + + def _get_shell(self, url_args: Dict) -> model.AssetAdministrationShell: + return self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) + def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) id_short = request.args.get("idShort") @@ -572,9 +596,7 @@ def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: return submodels def _get_submodel(self, url_args: Dict) -> model.Submodel: - submodel = self._get_obj_ts(url_args["submodel_id"], model.Submodel) - submodel.update() - return submodel + return self._get_obj_ts(url_args["submodel_id"], model.Submodel) def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: @@ -605,23 +627,7 @@ def handle_request(self, request: Request): # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) - - id_short = request.args.get("idShort") - if id_short is not None: - aas = filter(lambda shell: shell.id_short == id_short, aas) - - asset_ids = request.args.getlist("assetIds") - if asset_ids is not None: - # Decode and instantiate SpecificAssetIds - # This needs to be a list, otherwise we can only iterate it once. - specific_asset_ids: List[model.SpecificAssetId] = list( - map(lambda asset_id: HTTPApiDecoder.base64urljson(asset_id, model.SpecificAssetId, False), asset_ids)) - # Filter AAS based on these SpecificAssetIds - aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id - for specific_asset_id in specific_asset_ids), aas) - - return response_t(list(aas)) + return response_t(list(self._get_shells(request))) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -638,22 +644,20 @@ def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - self.object_store.remove(self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell)) + self.object_store.remove(self._get_shell(url_args)) return response_t() # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content parameter response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) return response_t(aas, stripped=is_stripped_request(request)) def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: # TODO: support content parameter response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) aas.update_from(HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, is_stripped_request(request))) aas.commit() @@ -661,29 +665,24 @@ def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: def get_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) return response_t(aas.asset_information) def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) aas.asset_information = HTTPApiDecoder.request_body(request, model.AssetInformation, False) aas.commit() return response_t() def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) return response_t(list(aas.submodel)) def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas_identifier = url_args["aas_id"] - aas = self._get_obj_ts(aas_identifier, model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) sm_ref = HTTPApiDecoder.request_body(request, model.ModelReference, False) if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") @@ -693,16 +692,14 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) aas.submodel.remove(self._get_submodel_reference(aas, url_args["submodel_id"])) aas.commit() return response_t() def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) submodel = self._resolve_reference(sm_ref) new_submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) @@ -719,8 +716,7 @@ def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kw def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) submodel = self._resolve_reference(sm_ref) self.object_store.remove(submodel) @@ -729,8 +725,7 @@ def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, ** return response_t() def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - aas.update() + aas = self._get_shell(url_args) # the following makes sure the reference exists self._get_submodel_reference(aas, url_args["submodel_id"]) redirect_url = map_adapter.build(self.get_submodel, { From 2a5a36064932e4316fb6255dec0358561dcb8ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 3 Apr 2024 15:41:23 +0200 Subject: [PATCH 135/157] adapter.http: remove outdated TODOs --- basyx/aas/adapter/http.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 8790bd6cc..a709ab886 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -34,9 +34,6 @@ from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union -# TODO: support the path/reference/etc. parameter - - @enum.unique class MessageType(enum.Enum): UNDEFINED = enum.auto() @@ -643,14 +640,12 @@ def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content parameter response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() return response_t(aas, stripped=is_stripped_request(request)) def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: support content parameter response_t = get_response_type(request) aas = self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) aas.update() @@ -804,22 +799,16 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: return response_t() def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec - # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec - # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) return response_t(list(submodel.submodel_element), stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - # TODO: the parentPath parameter is unnecessary for this route and should be removed from the spec - # TODO: support content, extent, semanticId parameters response_t = get_response_type(request) submodel = self._get_submodel(url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in From 10f330128e93d28c2d91b5e8e997f4eac9713348 Mon Sep 17 00:00:00 2001 From: Frosty2500 <125310380+Frosty2500@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:59:21 +0200 Subject: [PATCH 136/157] adapter.http: implement the attachment routes (#33) * adapter.http: implement the attachment routes * adapter.http: fix codestyle errors * adapter.http: implement recommended changes * adapter.http: implement new recommended changes --- basyx/aas/adapter/http.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index dfd554670..db3cd716e 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -25,6 +25,7 @@ from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response +from werkzeug.datastructures import FileStorage from basyx.aas import model from ._generic import XML_NS_MAP @@ -445,6 +446,14 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), + Submount("/attachment", [ + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_element_attachment), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_attachment), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_attachment), + ]), Submount("/qualifiers", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), @@ -875,6 +884,35 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) return response_t() + def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + if not isinstance(submodel_element, model.Blob): + raise BadRequest(f"{submodel_element!r} is not a blob, no file content to download!") + return Response(submodel_element.value, content_type=submodel_element.content_type) + + def put_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + file_storage: Optional[FileStorage] = request.files.get('file') + if file_storage is None: + raise BadRequest(f"Missing file to upload") + if not isinstance(submodel_element, model.Blob): + raise BadRequest(f"{submodel_element!r} is not a blob, no file content to update!") + submodel_element.value = file_storage.read() + submodel_element.commit() + return response_t() + + def delete_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) \ + -> Response: + response_t = get_response_type(request) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + if not isinstance(submodel_element, model.Blob): + raise BadRequest(f"{submodel_element!r} is not a blob, no file content to delete!") + submodel_element.value = None + submodel_element.commit() + return response_t() + def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: response_t = get_response_type(request) From b591b32da2566791d4813104efa3c118582725d1 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Fri, 5 Apr 2024 15:56:36 +0200 Subject: [PATCH 137/157] adapter.http: implement the AAS reference routes --- basyx/aas/adapter/http.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index dfd554670..2009e2de9 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -391,10 +391,12 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), Rule("/", methods=["POST"], endpoint=self.post_aas), + Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_all_reference), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_aas), Rule("/", methods=["PUT"], endpoint=self.put_aas), Rule("/", methods=["DELETE"], endpoint=self.delete_aas), + Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_reference), Submount("/asset-information", [ Rule("/", methods=["GET"], endpoint=self.get_aas_asset_information), Rule("/", methods=["PUT"], endpoint=self.put_aas_asset_information), @@ -639,16 +641,24 @@ def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> }, force_external=True) return response_t(aas, status=201, headers={"Location": created_resource_url}) - def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + def get_aas_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - self.object_store.remove(self._get_shell(url_args)) - return response_t() + aashells = self._get_shells(request) + references: list[model.ModelReference] = [model.ModelReference.from_referable(aas) + for aas in aashells] + return response_t(references) # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_shell(url_args) - return response_t(aas, stripped=is_stripped_request(request)) + return response_t(aas) + + def get_aas_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + aas = self._get_shell(url_args) + reference = model.ModelReference.from_referable(aas) + return response_t(reference) def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) @@ -658,6 +668,11 @@ def put_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: aas.commit() return response_t() + def delete_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + self.object_store.remove(self._get_shell(url_args)) + return response_t() + def get_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_shell(url_args) From 1b868655c11fb1a61614d706b5ce54fc77029f50 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Fri, 19 Apr 2024 16:06:46 +0200 Subject: [PATCH 138/157] adapter.http: implement the pagination --- basyx/aas/adapter/http.py | 117 +++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 41 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index bc013c7c1..0a9af8911 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -32,7 +32,7 @@ from .xml import XMLConstructables, read_aas_xml_element, xml_serialization, object_to_xml_element from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder -from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union +from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple @enum.unique @@ -100,15 +100,16 @@ class StrippedResultToJsonEncoder(ResultToJsonEncoder): class APIResponse(abc.ABC, Response): @abc.abstractmethod - def __init__(self, obj: Optional[ResponseData] = None, stripped: bool = False, *args, **kwargs): + def __init__(self, obj: Optional[ResponseData] = None, cursor: Optional[int] = None, + stripped: bool = False, *args, **kwargs): super().__init__(*args, **kwargs) if obj is None: self.status_code = 204 else: - self.data = self.serialize(obj, stripped) + self.data = self.serialize(obj, cursor, stripped) @abc.abstractmethod - def serialize(self, obj: ResponseData, stripped: bool) -> str: + def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: pass @@ -116,8 +117,16 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, obj: ResponseData, stripped: bool) -> str: - return json.dumps(obj, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, + def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: + data: Dict[str, Any] = {} + + # Add paging metadata if cursor is not None + if cursor is not None: + data["paging_metadata"] = {"cursor": cursor} + + # Add the result + data["result"] = obj + return json.dumps(data, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, separators=(",", ":")) @@ -125,25 +134,29 @@ class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, obj: ResponseData, stripped: bool) -> str: - # TODO: xml serialization doesn't support stripped objects + def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: + # Create the root XML element + root_elem = etree.Element("response", nsmap=XML_NS_MAP) + # Add the cursor as an attribute of the root element + root_elem.set("cursor", str(cursor)) + # Serialize the obj to XML if isinstance(obj, Result): - response_elem = result_to_xml(obj, nsmap=XML_NS_MAP) - etree.cleanup_namespaces(response_elem) + obj_elem = result_to_xml(obj, **XML_NS_MAP) # Assuming XML_NS_MAP is a namespace mapping else: if isinstance(obj, list): - response_elem = etree.Element("list", nsmap=XML_NS_MAP) - for obj in obj: - response_elem.append(object_to_xml_element(obj)) - etree.cleanup_namespaces(response_elem) + obj_elem = etree.Element("list", nsmap=XML_NS_MAP) + for item in obj: + item_elem = object_to_xml_element(item) + obj_elem.append(item_elem) else: - # dirty hack to be able to use the namespace prefixes defined in xml_serialization.NS_MAP - parent = etree.Element("parent", nsmap=XML_NS_MAP) - response_elem = object_to_xml_element(obj) - parent.append(response_elem) - etree.cleanup_namespaces(parent) - return etree.tostring(response_elem, xml_declaration=True, encoding="utf-8") - + obj_elem = object_to_xml_element(obj) + # Add the obj XML element to the root + root_elem.append(obj_elem) + # Clean up namespaces + etree.cleanup_namespaces(root_elem) + # Serialize the XML tree to a string + xml_str = etree.tostring(root_elem, xml_declaration=True, encoding="utf-8") + return xml_str class XmlResponseAlt(XmlResponse): def __init__(self, *args, content_type="text/xml", **kwargs): @@ -591,21 +604,38 @@ def _get_shells(self, request: Request) -> Iterator[model.AssetAdministrationShe def _get_shell(self, url_args: Dict) -> model.AssetAdministrationShell: return self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) - def _get_submodels(self, request: Request) -> Iterator[model.Submodel]: - submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) + def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], int]: + limit = request.args.get('limit', type=int, default=10) + cursor = request.args.get('cursor', type=int, default=0) + submodels: List[model.Submodel] = list(self._get_all_obj_of_type(model.Submodel)) + # Apply pagination + start_index = cursor + end_index = cursor + limit + paginated_submodels = submodels[start_index:end_index] id_short = request.args.get("idShort") if id_short is not None: - submodels = filter(lambda sm: sm.id_short == id_short, submodels) + paginated_submodels = filter(lambda sm: sm.id_short == id_short, paginated_submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: - spec_semantic_id = (HTTPApiDecoder.base64urljson - (semantic_id, model.Reference, False)) # type: ignore[type-abstract] - submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) - return submodels + spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) + paginated_submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, paginated_submodels) + return [paginated_submodels, end_index] def _get_submodel(self, url_args: Dict) -> model.Submodel: return self._get_obj_ts(url_args["submodel_id"], model.Submodel) + def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ + Tuple[List[model.SubmodelElement], int]: + limit = request.args.get('limit', type=int, default=10) + cursor = request.args.get('cursor', type=int, default=0) + submodel = self._get_submodel(url_args) + submodelelements = list(submodel.submodel_element) + # Apply pagination + start_index = cursor + end_index = cursor + limit + paginated_submodelelements = submodelelements[start_index:end_index] + return [paginated_submodelelements, end_index] + def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: submodel = self._get_submodel(url_args) @@ -759,8 +789,9 @@ def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapt # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request) - return response_t(list(submodels), stripped=is_stripped_request(request)) + submodels = self._get_submodels(request)[0] + cursor = self._get_submodels(request)[1] + return response_t(list(submodels), cursor=cursor, stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -777,15 +808,17 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte def get_submodel_all_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request) - return response_t(list(submodels), stripped=True) + submodels = self._get_submodels(request)[0] + cursor = self._get_submodels(request)[1] + return response_t(list(submodels), cursor=cursor, stripped=True) def get_submodel_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request) + submodels = self._get_submodels(request)[0] references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] - return response_t(references, stripped=is_stripped_request(request)) + cursor = self._get_submodels(request)[1] + return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) # --------- SUBMODEL ROUTES --------- @@ -819,20 +852,22 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel(url_args) - return response_t(list(submodel.submodel_element), stripped=is_stripped_request(request)) + submodelelements = self._get_submodel_submodel_elements(request, url_args)[0] + cursor = self._get_submodel_submodel_elements(request, url_args)[1] + return response_t(submodelelements, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel(url_args) - return response_t(list(submodel.submodel_element), stripped=True) + submodelelements = self._get_submodel_submodel_elements(request, url_args)[0] + cursor = self._get_submodel_submodel_elements(request, url_args)[1] + return response_t(submodelelements, cursor=cursor, stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodel = self._get_submodel(url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in - submodel.submodel_element] - return response_t(references, stripped=is_stripped_request(request)) + self._get_submodel_submodel_elements(request, url_args)[0]] + cursor = self._get_submodel_submodel_elements(request, url_args)[1] + return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) From d323ecb9b0670e2237948d4dfdf14a535fe920ec Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Fri, 19 Apr 2024 16:24:12 +0200 Subject: [PATCH 139/157] adapter.http: fix codestyle errors --- basyx/aas/adapter/http.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 0a9af8911..d0c8e53bf 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -32,7 +32,7 @@ from .xml import XMLConstructables, read_aas_xml_element, xml_serialization, object_to_xml_element from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder -from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple +from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple, Any @enum.unique @@ -158,6 +158,7 @@ def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: xml_str = etree.tostring(root_elem, xml_declaration=True, encoding="utf-8") return xml_str + class XmlResponseAlt(XmlResponse): def __init__(self, *args, content_type="text/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) @@ -611,15 +612,15 @@ def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], in # Apply pagination start_index = cursor end_index = cursor + limit - paginated_submodels = submodels[start_index:end_index] + paginated_submodels: Iterator[model.Submodel] = iter(submodels[start_index:end_index]) id_short = request.args.get("idShort") if id_short is not None: paginated_submodels = filter(lambda sm: sm.id_short == id_short, paginated_submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) + spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) # type: ignore paginated_submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, paginated_submodels) - return [paginated_submodels, end_index] + return (paginated_submodels, end_index) def _get_submodel(self, url_args: Dict) -> model.Submodel: return self._get_obj_ts(url_args["submodel_id"], model.Submodel) @@ -634,7 +635,7 @@ def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ start_index = cursor end_index = cursor + limit paginated_submodelelements = submodelelements[start_index:end_index] - return [paginated_submodelelements, end_index] + return (paginated_submodelelements, end_index) def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: From 76d9db055c2e030236d3d7e493b45a8eec799c28 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Tue, 23 Apr 2024 13:33:26 +0200 Subject: [PATCH 140/157] adapter.http: implement recommended changes --- basyx/aas/adapter/http.py | 120 +++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 59 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index d0c8e53bf..22b38bd08 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -16,6 +16,7 @@ import enum import io import json +import itertools from lxml import etree # type: ignore import werkzeug.exceptions @@ -109,7 +110,7 @@ def __init__(self, obj: Optional[ResponseData] = None, cursor: Optional[int] = N self.data = self.serialize(obj, cursor, stripped) @abc.abstractmethod - def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: + def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: pass @@ -117,39 +118,44 @@ class JsonResponse(APIResponse): def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: - data: Dict[str, Any] = {} - - # Add paging metadata if cursor is not None - if cursor is not None: - data["paging_metadata"] = {"cursor": cursor} - - # Add the result - data["result"] = obj - return json.dumps(data, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, - separators=(",", ":")) + def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: + if cursor is None: + return json.dumps( + obj, + cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, + separators=(",", ":") + ) + data: Dict[str, Any] = { + "paging_metadata": {"cursor": cursor}, + "result": obj + } + return json.dumps( + data, + cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, + separators=(",", ":") + ) class XmlResponse(APIResponse): def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) - def serialize(self, obj: ResponseData, cursor: int, stripped: bool) -> str: + def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: # Create the root XML element root_elem = etree.Element("response", nsmap=XML_NS_MAP) - # Add the cursor as an attribute of the root element - root_elem.set("cursor", str(cursor)) + if cursor is not None: + # Add the cursor as an attribute of the root element + root_elem.set("cursor", str(cursor)) # Serialize the obj to XML if isinstance(obj, Result): obj_elem = result_to_xml(obj, **XML_NS_MAP) # Assuming XML_NS_MAP is a namespace mapping + elif isinstance(obj, list): + obj_elem = etree.Element("list", nsmap=XML_NS_MAP) + for item in obj: + item_elem = object_to_xml_element(item) + obj_elem.append(item_elem) else: - if isinstance(obj, list): - obj_elem = etree.Element("list", nsmap=XML_NS_MAP) - for item in obj: - item_elem = object_to_xml_element(item) - obj_elem.append(item_elem) - else: - obj_elem = object_to_xml_element(obj) + obj_elem = object_to_xml_element(obj) # Add the obj XML element to the root root_elem.append(obj_elem) # Clean up namespaces @@ -583,7 +589,16 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i return ref raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") - def _get_shells(self, request: Request) -> Iterator[model.AssetAdministrationShell]: + @classmethod + def _get_slice(cls, request: Request, iterator: Iterator) -> Tuple[Iterator, int]: + limit = request.args.get('limit', type=int, default=10) + cursor = request.args.get('cursor', type=int, default=0) + start_index = cursor + end_index = cursor + limit + paginated_slice = itertools.islice(iterator, start_index, end_index) + return paginated_slice, end_index + + def _get_shells(self, request: Request) -> Tuple[Iterator[model.AssetAdministrationShell], int]: aas: Iterator[model.AssetAdministrationShell] = self._get_all_obj_of_type(model.AssetAdministrationShell) id_short = request.args.get("idShort") @@ -600,42 +615,32 @@ def _get_shells(self, request: Request) -> Iterator[model.AssetAdministrationShe aas = filter(lambda shell: all(specific_asset_id in shell.asset_information.specific_asset_id for specific_asset_id in specific_asset_ids), aas) - return aas + paginated_aas, end_index = self._get_slice(request, aas) + return paginated_aas, end_index def _get_shell(self, url_args: Dict) -> model.AssetAdministrationShell: return self._get_obj_ts(url_args["aas_id"], model.AssetAdministrationShell) def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], int]: - limit = request.args.get('limit', type=int, default=10) - cursor = request.args.get('cursor', type=int, default=0) - submodels: List[model.Submodel] = list(self._get_all_obj_of_type(model.Submodel)) - # Apply pagination - start_index = cursor - end_index = cursor + limit - paginated_submodels: Iterator[model.Submodel] = iter(submodels[start_index:end_index]) + submodels: Iterator[model.Submodel] = self._get_all_obj_of_type(model.Submodel) id_short = request.args.get("idShort") if id_short is not None: - paginated_submodels = filter(lambda sm: sm.id_short == id_short, paginated_submodels) + submodels = filter(lambda sm: sm.id_short == id_short, submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) # type: ignore - paginated_submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, paginated_submodels) - return (paginated_submodels, end_index) + submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) + paginated_submodels, end_index = self._get_slice(request, submodels) + return paginated_submodels, end_index def _get_submodel(self, url_args: Dict) -> model.Submodel: return self._get_obj_ts(url_args["submodel_id"], model.Submodel) - def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ + def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ Tuple[List[model.SubmodelElement], int]: - limit = request.args.get('limit', type=int, default=10) - cursor = request.args.get('cursor', type=int, default=0) submodel = self._get_submodel(url_args) - submodelelements = list(submodel.submodel_element) - # Apply pagination - start_index = cursor - end_index = cursor + limit - paginated_submodelelements = submodelelements[start_index:end_index] - return (paginated_submodelelements, end_index) + paginated_submodelelements, end_index = self._get_slice(request, submodel.submodel_element) + return list(paginated_submodelelements), end_index def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: @@ -666,7 +671,8 @@ def handle_request(self, request: Request): # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - return response_t(list(self._get_shells(request))) + aashels, cursor = self._get_shells(request) + return response_t(list(aashels), cursor=cursor) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -683,10 +689,10 @@ def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> def get_aas_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aashells = self._get_shells(request) + aashells, cursor = self._get_shells(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(aas) for aas in aashells] - return response_t(references) + return response_t(references, cursor=cursor) # --------- AAS ROUTES --------- def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: @@ -728,7 +734,8 @@ def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_shell(url_args) - return response_t(list(aas.submodel)) + submodel_refs, cursor = self._get_slice(request, aas.submodel) + return response_t(list(submodel_refs), cursor=cursor) def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) @@ -790,8 +797,7 @@ def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapt # ------ SUBMODEL REPO ROUTES ------- def get_submodel_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request)[0] - cursor = self._get_submodels(request)[1] + submodels, cursor = self._get_submodels(request) return response_t(list(submodels), cursor=cursor, stripped=is_stripped_request(request)) def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: @@ -809,16 +815,14 @@ def post_submodel(self, request: Request, url_args: Dict, map_adapter: MapAdapte def get_submodel_all_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request)[0] - cursor = self._get_submodels(request)[1] + submodels, cursor = self._get_submodels(request) return response_t(list(submodels), cursor=cursor, stripped=True) def get_submodel_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodels = self._get_submodels(request)[0] + submodels, cursor = self._get_submodels(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] - cursor = self._get_submodels(request)[1] return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) # --------- SUBMODEL ROUTES --------- @@ -853,21 +857,19 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements = self._get_submodel_submodel_elements(request, url_args)[0] - cursor = self._get_submodel_submodel_elements(request, url_args)[1] + submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) return response_t(submodelelements, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements = self._get_submodel_submodel_elements(request, url_args)[0] - cursor = self._get_submodel_submodel_elements(request, url_args)[1] + submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) return response_t(submodelelements, cursor=cursor, stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) + submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in - self._get_submodel_submodel_elements(request, url_args)[0]] - cursor = self._get_submodel_submodel_elements(request, url_args)[1] + submodelelements] return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: From 8ca847dfd346f2a6caf7c534ba998f61d79b5b68 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Sun, 28 Apr 2024 16:18:51 +0200 Subject: [PATCH 141/157] adapter.http: implement new recommended changes --- basyx/aas/adapter/http.py | 55 ++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 22b38bd08..159bd01ab 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -120,15 +120,12 @@ def __init__(self, *args, content_type="application/json", **kwargs): def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: if cursor is None: - return json.dumps( - obj, - cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, - separators=(",", ":") - ) - data: Dict[str, Any] = { - "paging_metadata": {"cursor": cursor}, - "result": obj - } + data = obj + else: + data = { + "paging_metadata": {"cursor": cursor}, + "result": obj + } return json.dumps( data, cls=StrippedResultToJsonEncoder if stripped else ResultToJsonEncoder, @@ -141,26 +138,21 @@ def __init__(self, *args, content_type="application/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: - # Create the root XML element root_elem = etree.Element("response", nsmap=XML_NS_MAP) if cursor is not None: - # Add the cursor as an attribute of the root element root_elem.set("cursor", str(cursor)) - # Serialize the obj to XML if isinstance(obj, Result): - obj_elem = result_to_xml(obj, **XML_NS_MAP) # Assuming XML_NS_MAP is a namespace mapping + result_elem = result_to_xml(obj, **XML_NS_MAP) + root_elem.append(result_elem) elif isinstance(obj, list): - obj_elem = etree.Element("list", nsmap=XML_NS_MAP) for item in obj: item_elem = object_to_xml_element(item) - obj_elem.append(item_elem) + root_elem.append(item_elem) else: obj_elem = object_to_xml_element(obj) - # Add the obj XML element to the root - root_elem.append(obj_elem) - # Clean up namespaces + for child in obj_elem: + root_elem.append(child) etree.cleanup_namespaces(root_elem) - # Serialize the XML tree to a string xml_str = etree.tostring(root_elem, xml_declaration=True, encoding="utf-8") return xml_str @@ -590,7 +582,7 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") @classmethod - def _get_slice(cls, request: Request, iterator: Iterator) -> Tuple[Iterator, int]: + def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: limit = request.args.get('limit', type=int, default=10) cursor = request.args.get('cursor', type=int, default=0) start_index = cursor @@ -628,7 +620,8 @@ def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], in submodels = filter(lambda sm: sm.id_short == id_short, submodels) semantic_id = request.args.get("semanticId") if semantic_id is not None: - spec_semantic_id = HTTPApiDecoder.base64urljson(semantic_id, model.Reference, False) # type: ignore + spec_semantic_id = HTTPApiDecoder.base64urljson( + semantic_id, model.Reference, False) # type: ignore[type-abstract] submodels = filter(lambda sm: sm.semantic_id == spec_semantic_id, submodels) paginated_submodels, end_index = self._get_slice(request, submodels) return paginated_submodels, end_index @@ -637,10 +630,11 @@ def _get_submodel(self, url_args: Dict) -> model.Submodel: return self._get_obj_ts(url_args["submodel_id"], model.Submodel) def _get_submodel_submodel_elements(self, request: Request, url_args: Dict) ->\ - Tuple[List[model.SubmodelElement], int]: + Tuple[Iterator[model.SubmodelElement], int]: submodel = self._get_submodel(url_args) - paginated_submodelelements, end_index = self._get_slice(request, submodel.submodel_element) - return list(paginated_submodelelements), end_index + paginated_submodel_elements: Iterator[model.SubmodelElement] + paginated_submodel_elements, end_index = self._get_slice(request, submodel.submodel_element) + return paginated_submodel_elements, end_index def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) \ -> model.SubmodelElement: @@ -734,6 +728,7 @@ def put_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) def get_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) aas = self._get_shell(url_args) + submodel_refs: Iterator[model.ModelReference[model.Submodel]] submodel_refs, cursor = self._get_slice(request, aas.submodel) return response_t(list(submodel_refs), cursor=cursor) @@ -857,19 +852,19 @@ def put_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: def get_submodel_submodel_elements(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) - return response_t(submodelelements, cursor=cursor, stripped=is_stripped_request(request)) + submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) + return response_t(list(submodel_elements), cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) - return response_t(submodelelements, cursor=cursor, stripped=True) + submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) + return response_t(list(submodel_elements), cursor=cursor, stripped=True) def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - submodelelements, cursor = self._get_submodel_submodel_elements(request, url_args) + submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in - submodelelements] + list(submodel_elements)] return response_t(references, cursor=cursor, stripped=is_stripped_request(request)) def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: From 458e40193defc6fae85825af7f98b055d997d16f Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Tue, 30 Apr 2024 23:52:39 +0200 Subject: [PATCH 142/157] adapter.http: implement new recommended changes --- basyx/aas/adapter/http.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 159bd01ab..cde58b4ad 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -143,7 +143,8 @@ def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> root_elem.set("cursor", str(cursor)) if isinstance(obj, Result): result_elem = result_to_xml(obj, **XML_NS_MAP) - root_elem.append(result_elem) + for child in result_elem: + root_elem.append(child) elif isinstance(obj, list): for item in obj: item_elem = object_to_xml_element(item) @@ -585,6 +586,10 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: limit = request.args.get('limit', type=int, default=10) cursor = request.args.get('cursor', type=int, default=0) + limit_str= request.args.get('limit', type=str, default="10") + cursor_str= request.args.get('cursor', type=str, default="0") + if not limit_str.isdigit() or not cursor_str.isdigit(): + raise BadRequest("Cursor and limit must be positive integers!") start_index = cursor end_index = cursor + limit paginated_slice = itertools.islice(iterator, start_index, end_index) From d4cf4b29235c2ffe8707b0d16a44fd6228034564 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Thu, 9 May 2024 18:39:11 +0200 Subject: [PATCH 143/157] adapter.http: change the limit and cursor check --- basyx/aas/adapter/http.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index cde58b4ad..179efc5ce 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -584,11 +584,13 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i @classmethod def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: - limit = request.args.get('limit', type=int, default=10) - cursor = request.args.get('cursor', type=int, default=0) - limit_str= request.args.get('limit', type=str, default="10") - cursor_str= request.args.get('cursor', type=str, default="0") - if not limit_str.isdigit() or not cursor_str.isdigit(): + limit = request.args.get('limit', default="10") + cursor = request.args.get('cursor', default="0") + try: + limit, cursor = int(limit), int(cursor) + if limit < 0 or cursor < 0: + raise ValueError + except ValueError: raise BadRequest("Cursor and limit must be positive integers!") start_index = cursor end_index = cursor + limit From 9a09638f2bc77f2fa2913c6882cac3233aabf9b6 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Thu, 23 May 2024 20:45:31 +0200 Subject: [PATCH 144/157] adapter.http: implement warning for not implemented routes --- basyx/aas/adapter/http.py | 65 ++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 179efc5ce..8bba1a53f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -23,7 +23,7 @@ import werkzeug.routing import werkzeug.urls import werkzeug.utils -from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity +from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity, NotImplemented from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response from werkzeug.datastructures import FileStorage @@ -402,6 +402,12 @@ def __init__(self, object_store: model.AbstractObjectStore): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ Submount("/api/v3.0", [ + Submount("/serialization", [ + Rule("/", methods=["GET"], endpoint=self.not_implemented) + ]), + Submount("/description", [ + Rule("/", methods=["GET"], endpoint=self.not_implemented) + ]), Submount("/shells", [ Rule("/", methods=["GET"], endpoint=self.get_aas_all), Rule("/", methods=["POST"], endpoint=self.post_aas), @@ -414,6 +420,11 @@ def __init__(self, object_store: model.AbstractObjectStore): Submount("/asset-information", [ Rule("/", methods=["GET"], endpoint=self.get_aas_asset_information), Rule("/", methods=["PUT"], endpoint=self.put_aas_asset_information), + Submount("/thumbnail", [ + Rule("/", methods=["GET"], endpoint=self.not_implemented), + Rule("/", methods=["PUT"], endpoint=self.not_implemented), + Rule("/", methods=["DELETE"], endpoint=self.not_implemented) + ]) ]), Submount("/submodel-refs", [ Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), @@ -434,12 +445,19 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("/", methods=["POST"], endpoint=self.post_submodel), Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_all_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_all_reference), + Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodels_metadata), + Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodels_reference), + Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), Submount("/submodel-elements", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), Rule("/", methods=["POST"], @@ -448,6 +466,8 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.get_submodel_submodel_elements_metadata), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_reference), + Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), Submount("/", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path), @@ -457,17 +477,40 @@ def __init__(self, object_store: model.AbstractObjectStore): endpoint=self.put_submodel_submodel_elements_id_short_path), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), + Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), + Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), Submount("/attachment", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_attachment), Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_element_attachment), Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_attachment), + endpoint=self.delete_submodel_submodel_element_attachment) + ]), + Submount("/invoke", [ + Rule("/", methods=["POST"], endpoint=self.not_implemented), + Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) + ]), + Submount("/invoke-async", [ + Rule("/", methods=["POST"], endpoint=self.not_implemented), + Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) + ]), + Submount("/operation-status", [ + Rule("//", methods=["GET"], + endpoint=self.not_implemented) + ]), + Submount("/operation-results", [ + Rule("//", methods=["GET"], + endpoint=self.not_implemented), + Rule("//$value/", methods=["GET"], + endpoint=self.not_implemented) ]), Submount("/qualifiers", [ Rule("/", methods=["GET"], @@ -479,9 +522,9 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_qualifiers), + endpoint=self.delete_submodel_submodel_element_qualifiers) ]) - ]), + ]) ]), Submount("/qualifiers", [ Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), @@ -492,7 +535,7 @@ def __init__(self, object_store: model.AbstractObjectStore): Rule("//", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), Rule("//", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_qualifiers), + endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) ]) @@ -584,10 +627,10 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i @classmethod def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: - limit = request.args.get('limit', default="10") - cursor = request.args.get('cursor', default="0") + limit_str = request.args.get('limit', default="10") + cursor_str = request.args.get('cursor', default="0") try: - limit, cursor = int(limit), int(cursor) + limit, cursor = int(limit_str), int(cursor_str) if limit < 0 or cursor < 0: raise ValueError except ValueError: @@ -669,6 +712,12 @@ def handle_request(self, request: Request): except werkzeug.exceptions.NotAcceptable as e: return e + # ------ all not implemented ROUTES ------- + def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + raise NotImplemented(f"This route is not implemented!") + return response_t() + # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) From 6d47a736ae819fd12fe828081e4c521a9656891f Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Fri, 24 May 2024 13:54:40 +0200 Subject: [PATCH 145/157] adapter.http: remove unnecessary lines --- basyx/aas/adapter/http.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 8bba1a53f..1acd55e01 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -23,7 +23,7 @@ import werkzeug.routing import werkzeug.urls import werkzeug.utils -from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity, NotImplemented +from werkzeug.exceptions import BadRequest, Conflict, NotFound, UnprocessableEntity from werkzeug.routing import MapAdapter, Rule, Submount from werkzeug.wrappers import Request, Response from werkzeug.datastructures import FileStorage @@ -714,9 +714,8 @@ def handle_request(self, request: Request): # ------ all not implemented ROUTES ------- def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) - raise NotImplemented(f"This route is not implemented!") - return response_t() + raise werkzeug.exceptions.NotImplemented(f"This route is not implemented!") + # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: From ae1f6fe57a13d08998f377bbc827f25dbc17b65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 24 May 2024 20:43:36 +0200 Subject: [PATCH 146/157] adapter.http: remove excess blank line --- basyx/aas/adapter/http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 1acd55e01..40068a815 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -716,7 +716,6 @@ def handle_request(self, request: Request): def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: raise werkzeug.exceptions.NotImplemented(f"This route is not implemented!") - # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) From 9b7a7f688ceb3eb4237fd26406165d279ad45355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 14:56:16 +0200 Subject: [PATCH 147/157] adapter.aasx: allow deleting files from `SupplementaryFileContainer` `AbstractSupplementaryFileContainer` and `DictSupplementaryFileContainer` are extended by a `delete_file()` method, that allows deleting files from them. Since different files may have the same content, references to the files contents in `DictSupplementaryFileContainer._store` are tracked via `_store_refcount`. A files contents are only deleted from `_store`, if all filenames referring to these these contents are deleted, i.e. if the refcount reaches 0. --- basyx/aas/adapter/aasx.py | 22 ++++++++++++++++++++++ test/adapter/aasx/test_aasx.py | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/basyx/aas/adapter/aasx.py b/basyx/aas/adapter/aasx.py index 6fa1ac118..30bb3948a 100644 --- a/basyx/aas/adapter/aasx.py +++ b/basyx/aas/adapter/aasx.py @@ -778,6 +778,13 @@ def write_file(self, name: str, file: IO[bytes]) -> None: """ pass # pragma: no cover + @abc.abstractmethod + def delete_file(self, name: str) -> None: + """ + Deletes a file from this SupplementaryFileContainer given its name. + """ + pass # pragma: no cover + @abc.abstractmethod def __contains__(self, item: str) -> bool: """ @@ -802,18 +809,23 @@ def __init__(self): self._store: Dict[bytes, bytes] = {} # Maps file names to (sha256, content_type) self._name_map: Dict[str, Tuple[bytes, str]] = {} + # Tracks the number of references to _store keys, + # i.e. the number of different filenames referring to the same file + self._store_refcount: Dict[bytes, int] = {} def add_file(self, name: str, file: IO[bytes], content_type: str) -> str: data = file.read() hash = hashlib.sha256(data).digest() if hash not in self._store: self._store[hash] = data + self._store_refcount[hash] = 0 name_map_data = (hash, content_type) new_name = name i = 1 while True: if new_name not in self._name_map: self._name_map[new_name] = name_map_data + self._store_refcount[hash] += 1 return new_name elif self._name_map[new_name] == name_map_data: return new_name @@ -839,6 +851,16 @@ def get_sha256(self, name: str) -> bytes: def write_file(self, name: str, file: IO[bytes]) -> None: file.write(self._store[self._name_map[name][0]]) + def delete_file(self, name: str) -> None: + # The number of different files with the same content are kept track of via _store_refcount. + # The contents are only deleted, once the refcount reaches zero. + hash: bytes = self._name_map[name][0] + self._store_refcount[hash] -= 1 + if self._store_refcount[hash] == 0: + del self._store[hash] + del self._store_refcount[hash] + del self._name_map[name] + def __contains__(self, item: object) -> bool: return item in self._name_map diff --git a/test/adapter/aasx/test_aasx.py b/test/adapter/aasx/test_aasx.py index 2fe4e0f33..132a93540 100644 --- a/test/adapter/aasx/test_aasx.py +++ b/test/adapter/aasx/test_aasx.py @@ -54,6 +54,23 @@ def test_supplementary_file_container(self) -> None: container.write_file("/TestFile.pdf", file_content) self.assertEqual(hashlib.sha1(file_content.getvalue()).hexdigest(), "78450a66f59d74c073bf6858db340090ea72a8b1") + # Add same file again with different content_type to test reference counting + with open(__file__, 'rb') as f: + duplicate_file = container.add_file("/TestFile.pdf", f, "image/jpeg") + self.assertIn(duplicate_file, container) + + # Delete files + container.delete_file(new_name) + self.assertNotIn(new_name, container) + # File should still be accessible + container.write_file(duplicate_file, file_content) + + container.delete_file(duplicate_file) + self.assertNotIn(duplicate_file, container) + # File should now not be accessible anymore + with self.assertRaises(KeyError): + container.write_file(duplicate_file, file_content) + class AASXWriterTest(unittest.TestCase): def test_writing_reading_example_aas(self) -> None: From 367746c509f9a51ddb4fcac2a8cafb0633428e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 22:48:19 +0200 Subject: [PATCH 148/157] adapter.http: fix a `DeprecationWarning` DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC) --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 40068a815..8d269f157 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -54,7 +54,7 @@ def __init__(self, code: str, text: str, message_type: MessageType = MessageType self.code: str = code self.text: str = text self.message_type: MessageType = message_type - self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.utcnow() + self.timestamp: datetime.datetime = timestamp if timestamp is not None else datetime.datetime.now(datetime.UTC) class Result: From ed012d0a6cfbeec5159653a4d3e8dfd1dbd030a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 23:43:02 +0200 Subject: [PATCH 149/157] adapter.http: allow retrieving and modifying `File` attachments via API This change makes use of the `SupplementaryFileContainer` interface of the AASX adapter. It allows the API to operate seamlessly on AASX files, including the contained supplementary files, without having to access the filesystem. Furthermore, the support for the modification of `Blob` values is removed (the spec prohibits it). --- basyx/aas/adapter/http.py | 75 +++++++++++++++++++++++++++++++++------ test/adapter/test_http.py | 5 +-- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 40068a815..fec2ed3a4 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -32,6 +32,7 @@ from ._generic import XML_NS_MAP from .xml import XMLConstructables, read_aas_xml_element, xml_serialization, object_to_xml_element from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder +from . import aasx from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple, Any @@ -398,8 +399,9 @@ def to_python(self, value: str) -> List[str]: class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore): + def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.AbstractSupplementaryFileContainer): self.object_store: model.AbstractObjectStore = object_store + self.file_store: aasx.AbstractSupplementaryFileContainer = file_store self.url_map = werkzeug.routing.Map([ Submount("/api/v3.0", [ Submount("/serialization", [ @@ -989,19 +991,55 @@ def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_ def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) \ -> Response: submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) - if not isinstance(submodel_element, model.Blob): - raise BadRequest(f"{submodel_element!r} is not a blob, no file content to download!") - return Response(submodel_element.value, content_type=submodel_element.content_type) + if not isinstance(submodel_element, (model.Blob, model.File)): + raise BadRequest(f"{submodel_element!r} is not a Blob or File, no file content to download!") + if submodel_element.value is None: + raise NotFound(f"{submodel_element!r} has no attachment!") + + value: bytes + if isinstance(submodel_element, model.Blob): + value = submodel_element.value + else: + if not submodel_element.value.startswith("/"): + raise BadRequest(f"{submodel_element!r} references an external file: {submodel_element.value}") + bytes_io = io.BytesIO() + try: + self.file_store.write_file(submodel_element.value, bytes_io) + except KeyError: + raise NotFound(f"No such file: {submodel_element.value}") + value = bytes_io.getvalue() + + # Blob and File both have the content_type attribute + return Response(value, content_type=submodel_element.content_type) # type: ignore[attr-defined] def put_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + + # spec allows PUT only for File, not for Blob + if not isinstance(submodel_element, model.File): + raise BadRequest(f"{submodel_element!r} is not a File, no file content to update!") + + if submodel_element.value is not None: + raise Conflict(f"{submodel_element!r} already references a file!") + + filename = request.form.get('fileName') + if filename is None: + raise BadRequest(f"No 'fileName' specified!") + + if not filename.startswith("/"): + raise BadRequest(f"Given 'fileName' doesn't start with a slash (/): {filename}") + file_storage: Optional[FileStorage] = request.files.get('file') if file_storage is None: raise BadRequest(f"Missing file to upload") - if not isinstance(submodel_element, model.Blob): - raise BadRequest(f"{submodel_element!r} is not a blob, no file content to update!") - submodel_element.value = file_storage.read() + + if file_storage.mimetype != submodel_element.content_type: + raise werkzeug.exceptions.UnsupportedMediaType( + f"Request body is of type {file_storage.mimetype!r}, " + f"while {submodel_element!r} has content_type {submodel_element.content_type!r}!") + + submodel_element.value = self.file_store.add_file(filename, file_storage.stream, submodel_element.content_type) submodel_element.commit() return response_t() @@ -1009,9 +1047,23 @@ def delete_submodel_submodel_element_attachment(self, request: Request, url_args -> Response: response_t = get_response_type(request) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) - if not isinstance(submodel_element, model.Blob): - raise BadRequest(f"{submodel_element!r} is not a blob, no file content to delete!") - submodel_element.value = None + if not isinstance(submodel_element, (model.Blob, model.File)): + raise BadRequest(f"{submodel_element!r} is not a Blob or File, no file content to delete!") + + if submodel_element.value is None: + raise NotFound(f"{submodel_element!r} has no attachment!") + + if isinstance(submodel_element, model.Blob): + submodel_element.value = None + else: + if not submodel_element.value.startswith("/"): + raise BadRequest(f"{submodel_element!r} references an external file: {submodel_element.value}") + try: + self.file_store.delete_file(submodel_element.value) + except KeyError: + pass + submodel_element.value = None + submodel_element.commit() return response_t() @@ -1084,4 +1136,5 @@ def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args if __name__ == "__main__": from werkzeug.serving import run_simple from basyx.aas.examples.data.example_aas import create_full_example - run_simple("localhost", 8080, WSGIApp(create_full_example()), use_debugger=True, use_reloader=True) + run_simple("localhost", 8080, WSGIApp(create_full_example(), aasx.DictSupplementaryFileContainer()), + use_debugger=True, use_reloader=True) diff --git a/test/adapter/test_http.py b/test/adapter/test_http.py index 528d2873b..09dadf865 100644 --- a/test/adapter/test_http.py +++ b/test/adapter/test_http.py @@ -33,6 +33,7 @@ import hypothesis.strategies from basyx.aas import model +from basyx.aas.adapter.aasx import DictSupplementaryFileContainer from basyx.aas.adapter.http import WSGIApp from basyx.aas.examples.data.example_aas import create_full_example @@ -82,10 +83,10 @@ def _check_transformed(response, case): # load aas and submodel api specs AAS_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-aas.yaml", - app=WSGIApp(create_full_example())) + app=WSGIApp(create_full_example(), DictSupplementaryFileContainer())) SUBMODEL_SCHEMA = schemathesis.from_path(pathlib.Path(__file__).parent / "http-api-oas-submodel.yaml", - app=WSGIApp(create_full_example())) + app=WSGIApp(create_full_example(), DictSupplementaryFileContainer())) class APIWorkflowAAS(AAS_SCHEMA.as_state_machine()): # type: ignore From 5fd980ae730ea1be4b793895b368bbae7d3a5cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 13 Jun 2024 23:59:00 +0200 Subject: [PATCH 150/157] adapter.http: allow changing the API base path --- basyx/aas/adapter/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 40068a815..bec1a1a45 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -398,10 +398,10 @@ def to_python(self, value: str) -> List[str]: class WSGIApp: - def __init__(self, object_store: model.AbstractObjectStore): + def __init__(self, object_store: model.AbstractObjectStore, base_path: str = "/api/v3.0"): self.object_store: model.AbstractObjectStore = object_store self.url_map = werkzeug.routing.Map([ - Submount("/api/v3.0", [ + Submount(base_path, [ Submount("/serialization", [ Rule("/", methods=["GET"], endpoint=self.not_implemented) ]), From 8e66b2cdcd5a227895fd9ed6f38ba935a22f128f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 19 Jun 2024 17:27:03 +0200 Subject: [PATCH 151/157] adapter.http: remove nonfunctional 'Not Implemented' check This check was intended to return 501 instead of 404 for routes that haven't been implemented. However, we explicitly implement these routes to return 501 now anyway, returning 501 for all other paths would be semantically incorrect anyway and the check never worked. --- basyx/aas/adapter/http.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 77a4c1b4b..0d16f81e0 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -699,8 +699,6 @@ def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = map_adapter.match() - if endpoint is None: - raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") # TODO: remove this 'type: ignore' comment once the werkzeug type annotations have been fixed # https://github.com/pallets/werkzeug/issues/2836 return endpoint(request, values, map_adapter=map_adapter) # type: ignore[operator] From a6af1af002be0dcc24d63ccee93c03302930fef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 20 Jun 2024 14:59:07 +0200 Subject: [PATCH 152/157] adapter.http: fix `lxml` typing --- basyx/aas/adapter/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 77a4c1b4b..10a37197e 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -156,7 +156,7 @@ def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> root_elem.append(child) etree.cleanup_namespaces(root_elem) xml_str = etree.tostring(root_elem, xml_declaration=True, encoding="utf-8") - return xml_str + return xml_str # type: ignore[return-value] class XmlResponseAlt(XmlResponse): @@ -164,7 +164,7 @@ def __init__(self, *args, content_type="text/xml", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) -def result_to_xml(result: Result, **kwargs) -> etree.Element: +def result_to_xml(result: Result, **kwargs) -> etree._Element: result_elem = etree.Element("result", **kwargs) success_elem = etree.Element("success") success_elem.text = xml_serialization.boolean_to_xml(result.success) @@ -177,7 +177,7 @@ def result_to_xml(result: Result, **kwargs) -> etree.Element: return result_elem -def message_to_xml(message: Message) -> etree.Element: +def message_to_xml(message: Message) -> etree._Element: message_elem = etree.Element("message") message_type_elem = etree.Element("messageType") message_type_elem.text = str(message.message_type) From cb0c2818f1a28a6942681564cdacdb86ebe435b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 19 Jun 2024 16:56:17 +0200 Subject: [PATCH 153/157] chore: bump werkzeug to >=3.0.3 Werkzeug 3.0.3 contains a fix for [1], so by requiring at least 3.0.3, we can remove unnecessary 'type: ignore' comments from the http adapter. [1]: https://github.com/pallets/werkzeug/issues/2836 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8335659a0..e087cdd18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ python-dateutil>=2.8,<3.0 types-python-dateutil pyecma376-2>=0.2.4 urllib3>=1.26,<2.0 -Werkzeug~=3.0 +Werkzeug>=3.0.3,<4 schemathesis~=3.7 hypothesis~=6.13 lxml-stubs~=0.5.1 diff --git a/setup.py b/setup.py index cb1d0349c..1e0c43484 100755 --- a/setup.py +++ b/setup.py @@ -44,6 +44,6 @@ 'lxml>=4.2,<5', 'urllib3>=1.26,<2.0', 'pyecma376-2>=0.2.4', - 'Werkzeug~=3.0' + 'Werkzeug>=3.0.3,<4' ] ) From 5191fef64b535c9b721fd8e0308227971b05f4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Wed, 19 Jun 2024 17:03:01 +0200 Subject: [PATCH 154/157] adapter.http: improve type hints Remove 'type: ignore' comments now that we require werkzeug >=3.0.3 [1]. Furthermore, fix the type hint of `WSGIApp._get_slice()` and make two other 'type: ignore' comments more explicit. [1]: https://github.com/pallets/werkzeug/issues/2836 --- basyx/aas/adapter/http.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 10a37197e..3cc2dfe9b 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -5,10 +5,6 @@ # # SPDX-License-Identifier: MIT -# TODO: remove this once the werkzeug type annotations have been fixed -# https://github.com/pallets/werkzeug/issues/2836 -# mypy: disable-error-code="arg-type" - import abc import base64 import binascii @@ -18,7 +14,7 @@ import json import itertools -from lxml import etree # type: ignore +from lxml import etree import werkzeug.exceptions import werkzeug.routing import werkzeug.urls @@ -629,7 +625,7 @@ def _get_submodel_reference(cls, aas: model.AssetAdministrationShell, submodel_i raise NotFound(f"The AAS {aas!r} doesn't have a submodel reference to {submodel_id!r}!") @classmethod - def _get_slice(cls, request: Request, iterator: Iterator[T]) -> Tuple[Iterator[T], int]: + def _get_slice(cls, request: Request, iterator: Iterable[T]) -> Tuple[Iterator[T], int]: limit_str = request.args.get('limit', default="10") cursor_str = request.args.get('cursor', default="0") try: @@ -701,9 +697,7 @@ def handle_request(self, request: Request): endpoint, values = map_adapter.match() if endpoint is None: raise werkzeug.exceptions.NotImplemented("This route is not yet implemented.") - # TODO: remove this 'type: ignore' comment once the werkzeug type annotations have been fixed - # https://github.com/pallets/werkzeug/issues/2836 - return endpoint(request, values, map_adapter=map_adapter) # type: ignore[operator] + return endpoint(request, values, map_adapter=map_adapter) # any raised error that leaves this function will cause a 500 internal server error # so catch raised http exceptions and return them except werkzeug.exceptions.NotAcceptable as e: @@ -951,7 +945,8 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar raise BadRequest(f"{parent!r} is not a namespace, can't add child submodel element!") # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, # type: ignore + new_submodel_element = HTTPApiDecoder.request_body(request, + model.SubmodelElement, # type: ignore[type-abstract] is_stripped_request(request)) try: parent.add_referable(new_submodel_element) @@ -971,7 +966,8 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - new_submodel_element = HTTPApiDecoder.request_body(request, model.SubmodelElement, # type: ignore + new_submodel_element = HTTPApiDecoder.request_body(request, + model.SubmodelElement, # type: ignore[type-abstract] is_stripped_request(request)) submodel_element.update_from(new_submodel_element) submodel_element.commit() From 4afc6ea6ca0eac737432880c8fd34ba4cccacd0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Sat, 20 Jul 2024 00:06:10 +0200 Subject: [PATCH 155/157] adapter.http: remove unnecessary generator expression --- basyx/aas/adapter/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 6cbf7fc53..c16dc689c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -382,7 +382,7 @@ class IdShortPathConverter(werkzeug.routing.UnicodeConverter): id_short_sep = "." def to_url(self, value: List[str]) -> str: - return super().to_url(self.id_short_sep.join(id_short for id_short in value)) + return super().to_url(self.id_short_sep.join(value)) def to_python(self, value: str) -> List[str]: id_shorts = super().to_python(value).split(self.id_short_sep) From 980dd7dfa8c93344fc9fdbeab51132dab43ff804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 19 Jul 2024 18:13:33 +0200 Subject: [PATCH 156/157] adapter.http: remove trailing slashes from routes This removes trailing slashes (and redirects to paths with trailing slashes) from the API and makes it compatible with the PCF2 showcase and other webapps. Previously, all routes were implemented with a trailing slash, e.g. `/submodels/` instead of `/submodels`. While the API spec only specifies the routes without a trailing slash, this has the advantage of being compatible with requests to the path with a trailing slash and without trailing slash, as werkzeug redirects requests to the slash-terminated path, if available. However, this poses a problem with browsers that make use of [CORS preflight requests][1] (e.g. Chromium-based browsers). Here, before doing an actual API request, the browser sends an `OPTIONS` request to the path it wants to request. This is done to check potential CORS headers (e.g. `Access-Control-Allow-Origin`) for the path, without retrieving the actual data. Our implementation doesn't support `OPTIONS` requests, which is fine. After the browser has received the response to the preflight request (which may or may not have been successful), it attempts to retrieve the actual data by sending the request again with the correct request method (e.g. `GET`). With our server this request now results in a redirect, as we redirect to the path with a trailing slash appended. This is a problem, as the browser didn't send a CORS preflight request to the path it is now redirected to. It also doesn't attempt to send another CORS preflight request, as it already sent one, with the difference being the now slash-terminated path. Thus, following the redirect is prevented by CORS policy and the data fails to load. By making the routes available via non-slash-terminated paths we avoid the need for redirects, which makes the server compatible with webapps viewed in browsers that use preflight requests. Requests to slash-terminated paths will no longer work (they won't redirect to the path without trailing slash). This shouldn't be a problem though, as the API is only specified without trailing slashes anyway. --- basyx/aas/adapter/http.py | 198 ++++++++++++++++++-------------------- 1 file changed, 91 insertions(+), 107 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 6cbf7fc53..43c1f985f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -401,139 +401,123 @@ def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.Abs self.file_store: aasx.AbstractSupplementaryFileContainer = file_store self.url_map = werkzeug.routing.Map([ Submount(base_path, [ - Submount("/serialization", [ - Rule("/", methods=["GET"], endpoint=self.not_implemented) - ]), - Submount("/description", [ - Rule("/", methods=["GET"], endpoint=self.not_implemented) - ]), + Rule("/serialization", methods=["GET"], endpoint=self.not_implemented), + Rule("/description", methods=["GET"], endpoint=self.not_implemented), + Rule("/shells", methods=["GET"], endpoint=self.get_aas_all), + Rule("/shells", methods=["POST"], endpoint=self.post_aas), Submount("/shells", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_all), - Rule("/", methods=["POST"], endpoint=self.post_aas), - Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_all_reference), + Rule("/$reference", methods=["GET"], endpoint=self.get_aas_all_reference), + Rule("/", methods=["GET"], endpoint=self.get_aas), + Rule("/", methods=["PUT"], endpoint=self.put_aas), + Rule("/", methods=["DELETE"], endpoint=self.delete_aas), Submount("/", [ - Rule("/", methods=["GET"], endpoint=self.get_aas), - Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/", methods=["DELETE"], endpoint=self.delete_aas), - Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_reference), - Submount("/asset-information", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/", methods=["PUT"], endpoint=self.put_aas_asset_information), - Submount("/thumbnail", [ - Rule("/", methods=["GET"], endpoint=self.not_implemented), - Rule("/", methods=["PUT"], endpoint=self.not_implemented), - Rule("/", methods=["DELETE"], endpoint=self.not_implemented) - ]) - ]), - Submount("/submodel-refs", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific) - ]), - Submount("/submodels/", [ - Rule("/", methods=["PUT"], endpoint=self.put_aas_submodel_refs_submodel), - Rule("/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_submodel), - Rule("/", endpoint=self.aas_submodel_refs_redirect), - Rule("//", endpoint=self.aas_submodel_refs_redirect) + Rule("/$reference", methods=["GET"], endpoint=self.get_aas_reference), + Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), + Rule("/asset-information/thumbnail", methods=["GET", "PUT", "DELETE"], + endpoint=self.not_implemented), + Rule("/submodel-refs", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("/submodel-refs", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("/submodel-refs/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific), + Submount("/submodels", [ + Rule("/", methods=["PUT"], + endpoint=self.put_aas_submodel_refs_submodel), + Rule("/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_submodel), + Rule("/", endpoint=self.aas_submodel_refs_redirect), + Rule("//", endpoint=self.aas_submodel_refs_redirect) ]) ]) ]), + Rule("/submodels", methods=["GET"], endpoint=self.get_submodel_all), + Rule("/submodels", methods=["POST"], endpoint=self.post_submodel), Submount("/submodels", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_all), - Rule("/", methods=["POST"], endpoint=self.post_submodel), - Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_all_metadata), - Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_all_reference), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_all_metadata), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_all_reference), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/", methods=["GET"], endpoint=self.get_submodel), + Rule("/", methods=["PUT"], endpoint=self.put_submodel), + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Submount("/", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), - Rule("/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodels_metadata), - Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$reference/", methods=["GET"], endpoint=self.get_submodels_reference), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), + Rule("/$metadata", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodels_reference), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/submodel-elements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodel-elements", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), Submount("/submodel-elements", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/$metadata/", methods=["GET"], + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_metadata), - Rule("/$reference/", methods=["GET"], + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_submodel_elements_reference), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_id_short_path), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Submount("/", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_id_short_path), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$metadata/", methods=["GET"], + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), - Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$reference/", methods=["GET"], + Rule("/$metadata", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), - Submount("/attachment", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_attachment), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_attachment), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_attachment) - ]), - Submount("/invoke", [ - Rule("/", methods=["POST"], endpoint=self.not_implemented), - Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) - ]), - Submount("/invoke-async", [ - Rule("/", methods=["POST"], endpoint=self.not_implemented), - Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) - ]), - Submount("/operation-status", [ - Rule("//", methods=["GET"], - endpoint=self.not_implemented) - ]), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/attachment", methods=["GET"], + endpoint=self.get_submodel_submodel_element_attachment), + Rule("/attachment", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_attachment), + Rule("/attachment", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_attachment), + Rule("/invoke", methods=["POST"], endpoint=self.not_implemented), + Rule("/invoke/$value", methods=["POST"], endpoint=self.not_implemented), + Rule("/invoke-async", methods=["POST"], endpoint=self.not_implemented), + Rule("/invoke-async/$value", methods=["POST"], endpoint=self.not_implemented), + Rule("/operation-status/", methods=["GET"], + endpoint=self.not_implemented), Submount("/operation-results", [ - Rule("//", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.not_implemented), - Rule("//$value/", methods=["GET"], + Rule("//$value", methods=["GET"], endpoint=self.not_implemented) ]), + Rule("/qualifiers", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("/qualifiers", methods=["POST"], + endpoint=self.post_submodel_submodel_element_qualifiers), Submount("/qualifiers", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) ]), + Rule("/qualifiers", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("/qualifiers", methods=["POST"], + endpoint=self.post_submodel_submodel_element_qualifiers), Submount("/qualifiers", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) @@ -542,7 +526,7 @@ def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.Abs ], converters={ "base64url": Base64URLConverter, "id_short_path": IdShortPathConverter - }) + }, strict_slashes=False) # TODO: the parameters can be typed via builtin wsgiref with Python 3.11+ def __call__(self, environ, start_response) -> Iterable[bytes]: From 4aa818c111cbcd5598100fd3367436a3bde78474 Mon Sep 17 00:00:00 2001 From: Frosty2500 <125310380+Frosty2500@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:29:15 +0200 Subject: [PATCH 157/157] adapter.http: add documentation of not implemented features (#52) This adds a module docstring to `adapter.http`, that details which features from the Specification of the Asset Administration Shell Part 2 (API) were not implemented. --- basyx/aas/adapter/http.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 134d8571a..3b2444990 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -4,6 +4,35 @@ # the LICENSE file of this project. # # SPDX-License-Identifier: MIT +""" +This module implements the "Specification of the Asset Administration Shell Part 2 Application Programming Interfaces". +However, several features and routes are currently not supported: + +1. Correlation ID: Not implemented because it was deemed unnecessary for this server. + +2. Extent Parameter (`withBlobValue/withoutBlobValue`): + Not implemented due to the lack of support in JSON/XML serialization. + +3. Route `/shells/{aasIdentifier}/asset-information/thumbnail`: Not implemented because the specification lacks clarity. + +4. Serialization and Description Routes: + - `/serialization` + - `/description` + These routes are not implemented at this time. + +5. Value, Path, and PATCH Routes: + - All `/…/value$`, `/…/path$`, and `PATCH` routes are currently not implemented. + +6. Operation Invocation Routes: The following routes are not implemented because operation invocation + is not yet supported by the `basyx-python-sdk`: + - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke` + - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke/$value` + - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async` + - `POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async/$value` + - `GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-status/{handleId}` + - `GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-results/{handleId}` + - `GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-results/{handleId}/$value` +""" import abc import base64