From 5d51d5e91a5f41057ac81e1a9bc0b7a21a9dc3be Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 27 Aug 2024 18:01:02 +0200 Subject: [PATCH 1/4] adapter.http: add CD-Repo routes --- basyx/aas/adapter/http.py | 57 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 3b244499..359f621c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -550,7 +550,14 @@ def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.Abs endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) - ]) + ]), + Rule("/concept-descriptions", methods=["GET"], endpoint=self.get_concept_description_all), + Rule("/concept-descriptions", methods=["POST"], endpoint=self.post_concept_description), + Submount("/concept-descriptions", [ + Rule("/", methods=["GET"], endpoint=self.get_concept_description), + Rule("/", methods=["PUT"], endpoint=self.put_concept_description), + Rule("/", methods=["DELETE"], endpoint=self.delete_concept_description), + ]), ]) ], converters={ "base64url": Base64URLConverter, @@ -730,8 +737,8 @@ def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Respon # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: response_t = get_response_type(request) - aashels, cursor = self._get_shells(request) - return response_t(list(aashels), cursor=cursor) + aashells, cursor = self._get_shells(request) + return response_t(list(aashells), cursor=cursor) def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) @@ -1143,9 +1150,53 @@ def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args sm_or_se.commit() return response_t() + # --------- CONCEPT DESCRIPTION ROUTES --------- + def get_concept_description_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + concept_descriptions: Iterator[model.ConceptDescription] = self._get_all_obj_of_type(model.ConceptDescription) + concept_descriptions, cursor = self._get_slice(request, concept_descriptions) + return response_t(list(concept_descriptions), cursor=cursor, stripped=is_stripped_request(request)) + + def post_concept_description(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: + response_t = get_response_type(request) + concept_description = HTTPApiDecoder.request_body(request, model.ConceptDescription, is_stripped_request(request)) + try: + self.object_store.add(concept_description) + except KeyError as e: + raise Conflict(f"ConceptDescription with Identifier {concept_description.id} already exists!") from e + concept_description.commit() + created_resource_url = map_adapter.build(self.get_concept_description, { + "concept_id": concept_description.id + }, force_external=True) + return response_t(concept_description, status=201, headers={"Location": created_resource_url}) + + def get_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + concept_description = self._get_concept_description(url_args) + return response_t(concept_description, stripped=is_stripped_request(request)) + + def _get_concept_description(self, url_args): + return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) + + def put_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + concept_description = self._get_concept_description(url_args) + concept_description.update_from(HTTPApiDecoder.request_body(request, model.ConceptDescription, + is_stripped_request(request))) + concept_description.commit() + return response_t() + + def delete_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: + response_t = get_response_type(request) + self.object_store.remove(self._get_concept_description(url_args)) + return response_t() + 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(), aasx.DictSupplementaryFileContainer()), use_debugger=True, use_reloader=True) + + +# Commit msg: Add CD-Repo routes to the server \ No newline at end of file From f5e1cf79a7ca0fb537f7a9a08285d3dd79a3c5c5 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 27 Aug 2024 18:10:48 +0200 Subject: [PATCH 2/4] adapter.http: code style fixes --- basyx/aas/adapter/http.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 359f621c..b679800c 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -1159,7 +1159,8 @@ def get_concept_description_all(self, request: Request, url_args: Dict, **_kwarg def post_concept_description(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: response_t = get_response_type(request) - concept_description = HTTPApiDecoder.request_body(request, model.ConceptDescription, is_stripped_request(request)) + concept_description = HTTPApiDecoder.request_body(request, model.ConceptDescription, + is_stripped_request(request)) try: self.object_store.add(concept_description) except KeyError as e: @@ -1197,6 +1198,3 @@ def delete_concept_description(self, request: Request, url_args: Dict, **_kwargs from basyx.aas.examples.data.example_aas import create_full_example run_simple("localhost", 8080, WSGIApp(create_full_example(), aasx.DictSupplementaryFileContainer()), use_debugger=True, use_reloader=True) - - -# Commit msg: Add CD-Repo routes to the server \ No newline at end of file From 3c74154ffa6d9634da1c81044468be4b43ce11cd Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:12:14 +0300 Subject: [PATCH 3/4] adapter.http: Refactor and improve codestyle (#292) This commit refactors the `adapter.http` module. - We remove `get_response_type()` in the beginning of all endpoint methods by integrating it in`handle_request()` and adding an argument for the response type in all endpoint methods. - Before, the `_get_submodel_or_nested_submodel_element()` method had `submodel` and `id_shorts` in its arguments, so in all methods that used it, a `submodel` and `id_short_path` needed to be explored with separate methods. We refactor those two arguments make the method an instance method. Now, it has only `url_args` as an argument and returns a `SubmodelElement` or a `Submodel`. --- basyx/aas/adapter/http.py | 288 +++++++++++++++++--------------------- 1 file changed, 129 insertions(+), 159 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index b679800c..4f07f43d 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -59,7 +59,7 @@ from .json import AASToJsonEncoder, StrictAASFromJsonDecoder, StrictStrippedAASFromJsonDecoder from . import aasx -from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple, Any +from typing import Callable, Dict, Iterable, Iterator, List, Optional, Type, TypeVar, Union, Tuple @enum.unique @@ -230,7 +230,7 @@ def get_response_type(request: Request) -> Type[APIResponse]: 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: " + raise werkzeug.exceptions.NotAcceptable("This server supports the following content types: " + ", ".join(response_types.keys())) return response_types[mime_type] @@ -329,7 +329,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_reference # type: ignore[assignment] + constructor = decoder._construct_reference # type: ignore[assignment] elif expect_type is model.Qualifier: constructor = decoder._construct_qualifier # type: ignore[assignment] @@ -343,8 +343,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: str, expect_type: Type[T], stripped: bool, expect_single: bool)\ - -> List[T]: + 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) @@ -605,11 +604,11 @@ def _get_nested_submodel_element(cls, namespace: model.UniqueIdShortNamespace, i 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]) \ - -> Union[model.Submodel, model.SubmodelElement]: + def _get_submodel_or_nested_submodel_element(self, url_args: Dict) -> Union[model.Submodel, model.SubmodelElement]: + submodel = self._get_submodel(url_args) + id_shorts: List[str] = url_args.get("id_shorts", []) try: - return cls._get_nested_submodel_element(submodel, id_shorts) + return self._get_nested_submodel_element(submodel, id_shorts) except ValueError: return submodel @@ -674,7 +673,7 @@ def _get_shells(self, request: Request) -> Tuple[Iterator[model.AssetAdministrat 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) + for specific_asset_id in specific_asset_ids), aas) paginated_aas, end_index = self._get_slice(request, aas) return paginated_aas, end_index @@ -698,50 +697,50 @@ def _get_submodels(self, request: Request) -> Tuple[Iterator[model.Submodel], in 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[Iterator[model.SubmodelElement], int]: submodel = self._get_submodel(url_args) 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: + def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) -> model.SubmodelElement: submodel = self._get_submodel(url_args) submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) return submodel_element + def _get_concept_description(self, url_args): + return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) + def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ) + try: + response_t = get_response_type(request) + except werkzeug.exceptions.NotAcceptable as e: + return e + try: endpoint, values = map_adapter.match() # 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, response_t=response_t, 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: - 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 + return http_exception_to_response(e, response_t) # ------ all not implemented ROUTES ------- def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: - raise werkzeug.exceptions.NotImplemented(f"This route is not implemented!") + raise werkzeug.exceptions.NotImplemented("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) + def get_aas_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: aashells, cursor = self._get_shells(request) return response_t(list(aashells), cursor=cursor) - def post_aas(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) + def post_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: aas = HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, False) try: self.object_store.add(aas) @@ -753,59 +752,56 @@ 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 get_aas_all_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas_all_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aashells, cursor = self._get_shells(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(aas) for aas in aashells] return response_t(references, cursor=cursor) # --------- AAS ROUTES --------- - def get_aas(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: aas = self._get_shell(url_args) return response_t(aas) - def get_aas_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: 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) + def put_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: aas = self._get_shell(url_args) aas.update_from(HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, is_stripped_request(request))) 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)) + def delete_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: + aas = self._get_shell(url_args) + self.object_store.remove(aas) return response_t() - def get_aas_asset_information(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_aas_asset_information(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: 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) + def put_aas_asset_information(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: 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) + def get_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: 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) - def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def post_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) sm_ref = HTTPApiDecoder.request_body(request, model.ModelReference, False) if sm_ref in aas.submodel: @@ -814,15 +810,15 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, **_kwargs) -> aas.commit() return response_t(sm_ref, status=201) - def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: 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) + def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) submodel = self._resolve_reference(sm_ref) @@ -838,8 +834,8 @@ def put_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, **_kw 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) + def delete_aas_submodel_refs_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: aas = self._get_shell(url_args) sm_ref = self._get_submodel_reference(aas, url_args["submodel_id"]) submodel = self._resolve_reference(sm_ref) @@ -862,13 +858,12 @@ def aas_submodel_refs_redirect(self, request: Request, url_args: Dict, map_adapt 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) + def get_submodel_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: 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: - response_t = get_response_type(request) + def post_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: submodel = HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)) try: self.object_store.add(submodel) @@ -880,13 +875,13 @@ 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_submodel_all_metadata(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodel_all_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: 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) + def get_submodel_all_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodels, cursor = self._get_submodels(request) references: list[model.ModelReference] = [model.ModelReference.from_referable(submodel) for submodel in submodels] @@ -894,74 +889,69 @@ def get_submodel_all_reference(self, request: Request, url_args: Dict, **_kwargs # --------- SUBMODEL ROUTES --------- - def delete_submodel(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def delete_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: 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: - response_t = get_response_type(request) + def get_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: 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) + def get_submodels_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodel = self._get_submodel(url_args) return response_t(submodel, stripped=True) - def get_submodels_reference(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_submodels_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: 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) + def put_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: submodel = self._get_submodel(url_args) 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: - response_t = get_response_type(request) + def get_submodel_submodel_elements(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: 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) + def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: 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) + def get_submodel_submodel_elements_reference(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) references: list[model.ModelReference] = [model.ModelReference.from_referable(element) for element in 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: - response_t = get_response_type(request) + def get_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], + **_kwargs) -> Response: 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: - response_t = get_response_type(request) + def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], **_kwargs) -> Response: 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: - response_t = get_response_type(request) + def get_submodel_submodel_elements_id_short_path_reference(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], **_kwargs) -> Response: 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): - response_t = get_response_type(request) - 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) + def post_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], + map_adapter: MapAdapter): + parent = self._get_submodel_or_nested_submodel_element(url_args) 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] @@ -976,14 +966,17 @@ def post_submodel_submodel_elements_id_short_path(self, request: Request, url_ar raise raise Conflict(f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " f"within {parent}!") + submodel = self._get_submodel(url_args) + id_short_path = url_args.get("id_shorts", []) created_resource_url = map_adapter.build(self.get_submodel_submodel_elements_id_short_path, { "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}) - def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def put_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], + **_kwargs) -> Response: 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 @@ -994,20 +987,15 @@ def put_submodel_submodel_elements_id_short_path(self, request: Request, url_arg submodel_element.commit() return response_t() - 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(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]), - id_short_path[-1] - ) - self._namespace_submodel_element_op(parent, parent.remove_referable, id_short_path[-1]) + def delete_submodel_submodel_elements_id_short_path(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], + **_kwargs) -> Response: + sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) + parent: model.UniqueIdShortNamespace = self._expect_namespace(sm_or_se.parent, sm_or_se.id_short) + self._namespace_submodel_element_op(parent, parent.remove_referable, sm_or_se.id_short) return response_t() - def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) \ - -> Response: + 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, model.File)): raise BadRequest(f"{submodel_element!r} is not a Blob or File, no file content to download!") @@ -1030,29 +1018,26 @@ def get_submodel_submodel_element_attachment(self, request: Request, url_args: D # 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) + def put_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: 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: + elif 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("No 'fileName' specified!") + elif 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 file_storage.mimetype != submodel_element.content_type: + raise BadRequest("Missing file to upload") + elif 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}!") @@ -1061,14 +1046,13 @@ def put_submodel_submodel_element_attachment(self, request: Request, url_args: D 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) + def delete_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], + **_kwargs) -> Response: submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) 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: + elif submodel_element.value is None: raise NotFound(f"{submodel_element!r} has no attachment!") if isinstance(submodel_element, model.Blob): @@ -1085,42 +1069,32 @@ def delete_submodel_submodel_element_attachment(self, request: Request, url_args 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) - submodel = self._get_submodel(url_args) - sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, url_args.get("id_shorts", [])) + def get_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: + sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) qualifier_type = url_args.get("qualifier_type") if qualifier_type is None: return response_t(list(sm_or_se.qualifier)) 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: - response_t = get_response_type(request) - submodel_identifier = url_args["submodel_id"] - 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) + def post_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: + sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) 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) sm_or_se.commit() 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, + "submodel_id": url_args["submodel_id"], + "id_shorts": url_args.get("id_shorts") or None, "qualifier_type": qualifier.type }, force_external=True) return response_t(qualifier, status=201, headers={"Location": created_resource_url}) - 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"] - 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) + def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: + sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) new_qualifier = HTTPApiDecoder.request_body(request, model.Qualifier, is_stripped_request(request)) qualifier_type = url_args["qualifier_type"] qualifier = self._qualifiable_qualifier_op(sm_or_se, sm_or_se.get_qualifier_by_type, qualifier_type) @@ -1132,33 +1106,31 @@ def put_submodel_submodel_element_qualifiers(self, request: Request, url_args: D sm_or_se.commit() if qualifier_type_changed: 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, + "submodel_id": url_args["submodel_id"], + "id_shorts": url_args.get("id_shorts") or None, "qualifier_type": new_qualifier.type }, force_external=True) return response_t(new_qualifier, status=201, headers={"Location": created_resource_url}) return response_t(new_qualifier) - 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) - id_shorts: List[str] = url_args.get("id_shorts", []) - sm_or_se = self._get_submodel_or_nested_submodel_element(submodel, id_shorts) + def delete_submodel_submodel_element_qualifiers(self, request: Request, url_args: Dict, + response_t: Type[APIResponse], + **_kwargs) -> Response: + sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) qualifier_type = url_args["qualifier_type"] self._qualifiable_qualifier_op(sm_or_se, sm_or_se.remove_qualifier_by_type, qualifier_type) sm_or_se.commit() return response_t() # --------- CONCEPT DESCRIPTION ROUTES --------- - def get_concept_description_all(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_concept_description_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: concept_descriptions: Iterator[model.ConceptDescription] = self._get_all_obj_of_type(model.ConceptDescription) concept_descriptions, cursor = self._get_slice(request, concept_descriptions) return response_t(list(concept_descriptions), cursor=cursor, stripped=is_stripped_request(request)) - def post_concept_description(self, request: Request, url_args: Dict, map_adapter: MapAdapter) -> Response: - response_t = get_response_type(request) + def post_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + map_adapter: MapAdapter) -> Response: concept_description = HTTPApiDecoder.request_body(request, model.ConceptDescription, is_stripped_request(request)) try: @@ -1171,24 +1143,21 @@ def post_concept_description(self, request: Request, url_args: Dict, map_adapter }, force_external=True) return response_t(concept_description, status=201, headers={"Location": created_resource_url}) - def get_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def get_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: concept_description = self._get_concept_description(url_args) return response_t(concept_description, stripped=is_stripped_request(request)) - def _get_concept_description(self, url_args): - return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) - - def put_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def put_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: concept_description = self._get_concept_description(url_args) concept_description.update_from(HTTPApiDecoder.request_body(request, model.ConceptDescription, is_stripped_request(request))) concept_description.commit() return response_t() - def delete_concept_description(self, request: Request, url_args: Dict, **_kwargs) -> Response: - response_t = get_response_type(request) + def delete_concept_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], + **_kwargs) -> Response: self.object_store.remove(self._get_concept_description(url_args)) return response_t() @@ -1196,5 +1165,6 @@ def delete_concept_description(self, request: Request, url_args: Dict, **_kwargs 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(), aasx.DictSupplementaryFileContainer()), use_debugger=True, use_reloader=True) From 208e6f931953db601578334b3d7fce8d9b03c808 Mon Sep 17 00:00:00 2001 From: hadijannat <111177329+hadijannat@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:46:09 +0200 Subject: [PATCH 4/4] Migrate from setup.py to pyproject.toml (#290) Migrate from setup.py to pyproject.toml Previously, we used `setup.py` for specifying our package, together with a `requirements.txt` for defining our dependencies. [PEP 518] introduces `pyproject.toml` as the new and suggested way of specifying both, build process and dependencies. With this commit, we migrate from the old `setup.py` and `requirements.txt` to using `pyproject.toml`. [PEP 518](https://peps.python.org/pep-0518/) --- .github/workflows/ci.yml | 4 ++-- pyproject.toml | 48 +++++++++++++++++++++++++++++++++++++++ setup.py | 49 ---------------------------------------- 3 files changed, 50 insertions(+), 51 deletions(-) create mode 100644 pyproject.toml delete mode 100755 setup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c633fe05..1e65ca3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,7 +125,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel + pip install setuptools wheel build - name: Create source and wheel dist run: | - python setup.py sdist bdist_wheel + python -m build diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..75aeea66 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "basyx-python-sdk" +version = "1.0.0" +description = "The Eclipse BaSyx Python SDK, an implementation of the Asset Administration Shell for Industry 4.0 systems" +authors = [ + { name = "The Eclipse BaSyx Authors", email = "admins@iat.rwth-aachen.de" } +] +readme = "README.md" +license = { file = "LICENSE" } +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Development Status :: 5 - Production/Stable" +] +requires-python = ">=3.8" +dependencies = [ + "python-dateutil>=2.8,<3", + "lxml>=4.2,<5", + "urllib3>=1.26,<2.0", + "pyecma376-2>=0.2.4" +] + +[project.optional-dependencies] +dev = [ + "mypy", + "pycodestyle", + "codeblocks", + "coverage", +] + +[project.urls] +"Homepage" = "https://github.com/eclipse-basyx/basyx-python-sdk" + +[tool.setuptools] +packages = ["basyx"] + +[tool.setuptools.package-data] +basyx = ["py.typed"] +"basyx.aas.examples.data" = ["TestFile.pdf"] + +[tool.setuptools.exclude-package-data] +"*" = ["test", "test.*"] + diff --git a/setup.py b/setup.py deleted file mode 100755 index 1e0c4348..00000000 --- a/setup.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2019-2024 the Eclipse BaSyx Authors -# -# This program and the accompanying materials are made available under the terms of the MIT License, available in -# the LICENSE file of this project. -# -# SPDX-License-Identifier: MIT - -import setuptools -from basyx.aas import __version__ - -with open("README.md", "r", encoding='utf-8') as fh: - long_description = fh.read() - -setuptools.setup( - name="basyx-python-sdk", - version=__version__, - author="The Eclipse BaSyx Authors", - description="The Eclipse BaSyx Python SDK, an implementation of the Asset Administration Shell for Industry 4.0 " - "systems", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/eclipse-basyx/basyx-python-sdk", - packages=setuptools.find_packages(exclude=["test", "test.*"]), - zip_safe=False, - package_data={ - "basyx": ["py.typed"], - "basyx.aas.examples.data": ["TestFile.pdf"], - }, - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Development Status :: 5 - Production/Stable", - ], - entry_points={ - 'console_scripts': [ - "aas-compliance-check = basyx.aas.compliance_tool.cli:main" - ] - }, - python_requires='>=3.8', - install_requires=[ - 'python-dateutil>=2.8,<3', - 'lxml>=4.2,<5', - 'urllib3>=1.26,<2.0', - 'pyecma376-2>=0.2.4', - 'Werkzeug>=3.0.3,<4' - ] -)