From eb2d2dd00fb6b5a51fbac82fb65bf52c15a8a582 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Tue, 10 Dec 2024 12:48:22 +0100 Subject: [PATCH 01/45] sdk/server: Add basic implementation of some endpoints Provide an example for the structure used Add DESIGN_NTOES.md further explaining design choices --- sdk/basyx/server/DESIGN_NOTES.md | 26 +++ sdk/basyx/server/__init__.py | 0 sdk/basyx/server/api/aas.py | 24 +++ sdk/basyx/server/api/submodel.py | 198 ++++++++++++++++++ sdk/basyx/server/server.py | 23 ++ sdk/basyx/server/services/aas_service.py | 6 + sdk/basyx/server/services/submodel_service.py | 36 ++++ 7 files changed, 313 insertions(+) create mode 100644 sdk/basyx/server/DESIGN_NOTES.md create mode 100644 sdk/basyx/server/__init__.py create mode 100644 sdk/basyx/server/api/aas.py create mode 100644 sdk/basyx/server/api/submodel.py create mode 100644 sdk/basyx/server/server.py create mode 100644 sdk/basyx/server/services/aas_service.py create mode 100644 sdk/basyx/server/services/submodel_service.py diff --git a/sdk/basyx/server/DESIGN_NOTES.md b/sdk/basyx/server/DESIGN_NOTES.md new file mode 100644 index 0000000..4777a54 --- /dev/null +++ b/sdk/basyx/server/DESIGN_NOTES.md @@ -0,0 +1,26 @@ +# Server Design Notes + +> [!warning] +> This concept is heavily WIP! Features presented here might not be implemented + +## General ideas +The server is implemented using `FastAPI`. +The project is divided into distinct modules (inspired +[by the server specification](https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2024/10/IDTA-01002-3-0-3_SpecificationAssetAdministrationShell_Part2_API.pdf#page=128) +) to enhance maintainability and readability. + +### API Classes + +The `api` module contains classes that define the endpoints exposed by the server. As little logic as possible is implemented here. + +### Service Classes +The `services` module contains all the necessary logic to enable the actions requested by the endpoints defined in `api`. + +### Shared Data +With this structure all the routers and services are standalone and do not access each other in any way. As endpoints +need to maintain context across service specifications, we use a central `ObjectStore` instance to handle +objects present during runtime. + +### server.py +The server.py contains the main class. Here we construct our `ObjectStore` instance and with that the routers. +As every router is standalone and adds onto the current set of endpoints, we can select which specifications we want to add on startup. \ No newline at end of file diff --git a/sdk/basyx/server/__init__.py b/sdk/basyx/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sdk/basyx/server/api/aas.py b/sdk/basyx/server/api/aas.py new file mode 100644 index 0000000..3c81e1c --- /dev/null +++ b/sdk/basyx/server/api/aas.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter +from typing import Any + +router = APIRouter() + + +@router.get("/aas") +async def get_all_aas() -> Any: + return {"message": ""} + + +@router.get("/aas/{aas_id}") +async def get_aas_by_id(aas_id: str) -> Any: + return {"message": ""} + + +@router.post("/aas") +async def create_aas() -> Any: + return {"message": ""} + + +@router.delete("/aas/{aas_id}") +async def delete_aas(aas_id: str) -> Any: + return {"message": ""} diff --git a/sdk/basyx/server/api/submodel.py b/sdk/basyx/server/api/submodel.py new file mode 100644 index 0000000..83f712e --- /dev/null +++ b/sdk/basyx/server/api/submodel.py @@ -0,0 +1,198 @@ +from typing import Any + +from aas_core3.types import Identifiable +from fastapi import APIRouter, Request, HTTPException + +from sdk.basyx import ObjectStore +from sdk.basyx.server.services.submodel_service import SubmodelService + + +class SubmodelRouter: + def __init__(self, global_obj_store: ObjectStore[Identifiable]): + self.router = APIRouter() + self.obj_store = global_obj_store + self.service = SubmodelService(global_obj_store) + self._setup_routes() + + def _setup_routes(self): + @self.router.get("/") + async def get_submodel_all() -> Any: + return self.service.get_all_submodels_as_jsonables() + + @self.router.post("/") + async def post_submodel(request: Request) -> Any: + body = await request.json() + return self.service.add_submodel_from_body(body) + + @self.router.get("/$metadata") + async def get_submodel_all_metadata() -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/$reference") + async def get_submodel_all_reference() -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/$value") + async def not_implemented_value() -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/$path") + async def not_implemented_path() -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}") + async def get_submodel(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.put("/{submodel_id}") + async def put_submodel(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.delete("/{submodel_id}") + async def delete_submodel(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.patch("/{submodel_id}") + async def not_implemented_patch_submodel(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + # Nested routes for each submodel + @self.router.get("/{submodel_id}/$metadata") + async def get_submodels_metadata(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.patch("/{submodel_id}/$metadata") + async def not_implemented_metadata_patch(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/$value") + async def not_implemented_value_get(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.patch("/{submodel_id}/$value") + async def not_implemented_value_patch(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/$reference") + async def get_submodels_reference(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/$path") + async def not_implemented_path_get(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements") + async def get_submodel_submodel_elements(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.post("/{submodel_id}/submodel-elements") + async def post_submodel_elements(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/$metadata") + async def get_submodel_submodel_elements_metadata(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/$reference") + async def get_submodel_submodel_elements_reference(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/$value") + async def not_implemented_submodel_elements_value(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/$path") + async def not_implemented_submodel_elements_path(submodel_id: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}") + async def get_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}") + async def post_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}") + async def put_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}") + async def delete_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}") + async def not_implemented_patch_submodel_submodel_elements_id_short_path(submodel_id: str, + id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/$metadata") + async def get_submodel_submodel_elements_id_short_path_metadata(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}/$metadata") + async def not_implemented_metadata_patch(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/$reference") + async def get_submodel_submodel_elements_id_short_path_reference(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/$value") + async def not_implemented_value_get(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}/$value") + async def not_implemented_value_patch(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/attachment") + async def get_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}/attachment") + async def put_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}/attachment") + async def delete_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke") + async def not_implemented_invoke(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke/$value") + async def not_implemented_invoke_value(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke-async") + async def not_implemented_invoke_async(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke-async/$value") + async def not_implemented_invoke_async_value(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers") + async def get_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers") + async def post_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}") + async def get_submodel_submodel_element_qualifiers_specific(submodel_id: str, id_shorts: str, + qualifier_type: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}") + async def put_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str, + qualifier_type: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") + + @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}") + async def delete_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str, + qualifier_type: str) -> Any: + raise HTTPException(status_code=501, detail="This route is not implemented!") diff --git a/sdk/basyx/server/server.py b/sdk/basyx/server/server.py new file mode 100644 index 0000000..bb6222b --- /dev/null +++ b/sdk/basyx/server/server.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI +import uvicorn +from pygments.lexers import q + +# Import routers +from api import submodel + +from sdk.basyx import object_store + +app = FastAPI() +prefix = "/api/v3.0" + +central_object_store = object_store.ObjectStore() + +submodel_router = submodel.SubmodelRouter(central_object_store) + +# Register router +# TODO: This can be done dynamically based on startup params +app.include_router(submodel_router.router, prefix=prefix + "/submodels") + +# Start the server if this file is executed directly +if __name__ == "__main__": + uvicorn.run("server:app", host="127.0.0.1", port=8000, reload=True) diff --git a/sdk/basyx/server/services/aas_service.py b/sdk/basyx/server/services/aas_service.py new file mode 100644 index 0000000..d3ffccf --- /dev/null +++ b/sdk/basyx/server/services/aas_service.py @@ -0,0 +1,6 @@ +from sdk.basyx import ObjectStore + + +class aas_service: + def __init__(self, global_object_store: ObjectStore): + self.obj_store = global_object_store diff --git a/sdk/basyx/server/services/submodel_service.py b/sdk/basyx/server/services/submodel_service.py new file mode 100644 index 0000000..fbb68e3 --- /dev/null +++ b/sdk/basyx/server/services/submodel_service.py @@ -0,0 +1,36 @@ +from typing import Any, MutableMapping + +from aas_core3 import jsonization +from aas_core3.types import Submodel +from fastapi import HTTPException + +from sdk.basyx import ObjectStore + + +class SubmodelService: + def __init__(self, global_object_store: ObjectStore): + self.obj_store = global_object_store + + # General helper functions + def _get_all_submodels(self) -> list[Submodel]: + return [item for item in self.obj_store if isinstance(item, Submodel)] + + def _jsonable_submodels(self, submodels: list[Submodel]) \ + -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + return [jsonization.to_jsonable(submodel) for submodel in submodels] + + # Endpoint specific logic + def get_all_submodels_as_jsonables(self)\ + -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + return self._jsonable_submodels(self._get_all_submodels()) + + def add_submodel_from_body(self, json): + submodel = jsonization.submodel_from_jsonable(json) + try: + self.obj_store.add(submodel) + except KeyError as e: + # TODO: Provide a stacktrace) + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "Submodel processed"} + From fe23a748fd06ea87fb24de5925949b578f507a9b Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Tue, 10 Dec 2024 14:20:38 +0100 Subject: [PATCH 02/45] /sdk/basyx/serer/submodel: Add more endpoints Adjust not implemented message to distinguish between endpoints "not yet implemented" and endpoints that were "not implemented" in the old basyx-python-sdk --- sdk/basyx/server/api/submodel.py | 85 ++++++++++--------- sdk/basyx/server/services/submodel_service.py | 30 ++++++- 2 files changed, 74 insertions(+), 41 deletions(-) diff --git a/sdk/basyx/server/api/submodel.py b/sdk/basyx/server/api/submodel.py index 83f712e..b5d4a09 100644 --- a/sdk/basyx/server/api/submodel.py +++ b/sdk/basyx/server/api/submodel.py @@ -26,11 +26,13 @@ async def post_submodel(request: Request) -> Any: @self.router.get("/$metadata") async def get_submodel_all_metadata() -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + # Returns metadata for all submodels, stripped of detailed content + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/$reference") async def get_submodel_all_reference() -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + # Returns references for all submodels without full data + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/$value") async def not_implemented_value() -> Any: @@ -42,15 +44,17 @@ async def not_implemented_path() -> Any: @self.router.get("/{submodel_id}") async def get_submodel(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + return self.service.get_submodel_jsonable_by_id(submodel_id) @self.router.put("/{submodel_id}") - async def put_submodel(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + async def put_submodel(submodel_id: str, request: Request) -> Any: + # Update submodel with given id + body = await request.json() + return self.service.update_submode_by_id(submodel_id, body) @self.router.delete("/{submodel_id}") async def delete_submodel(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + return self.service.delete_submodel_by_id(submodel_id) @self.router.patch("/{submodel_id}") async def not_implemented_patch_submodel(submodel_id: str) -> Any: @@ -59,140 +63,141 @@ async def not_implemented_patch_submodel(submodel_id: str) -> Any: # Nested routes for each submodel @self.router.get("/{submodel_id}/$metadata") async def get_submodels_metadata(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.patch("/{submodel_id}/$metadata") async def not_implemented_metadata_patch(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/$value") async def not_implemented_value_get(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.patch("/{submodel_id}/$value") async def not_implemented_value_patch(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/$reference") async def get_submodels_reference(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/$path") async def not_implemented_path_get(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements") async def get_submodel_submodel_elements(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + # Get submodel elements + self.service.get_submodel_elements(submodel_id) @self.router.post("/{submodel_id}/submodel-elements") async def post_submodel_elements(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/$metadata") async def get_submodel_submodel_elements_metadata(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/$reference") async def get_submodel_submodel_elements_reference(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/$value") async def not_implemented_submodel_elements_value(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/$path") async def not_implemented_submodel_elements_path(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}") async def get_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}") async def post_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}") async def put_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}") async def delete_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}") async def not_implemented_patch_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/$metadata") async def get_submodel_submodel_elements_id_short_path_metadata(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}/$metadata") async def not_implemented_metadata_patch(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/$reference") async def get_submodel_submodel_elements_id_short_path_reference(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/$value") async def not_implemented_value_get(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}/$value") async def not_implemented_value_patch(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/attachment") async def get_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}/attachment") async def put_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}/attachment") async def delete_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke") async def not_implemented_invoke(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke/$value") async def not_implemented_invoke_value(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke-async") async def not_implemented_invoke_async(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke-async/$value") async def not_implemented_invoke_async_value(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers") async def get_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers") async def post_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}") async def get_submodel_submodel_element_qualifiers_specific(submodel_id: str, id_shorts: str, qualifier_type: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}") async def put_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str, qualifier_type: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}") async def delete_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str, qualifier_type: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not implemented!") + raise HTTPException(status_code=501, detail="This route is not yet implemented!") diff --git a/sdk/basyx/server/services/submodel_service.py b/sdk/basyx/server/services/submodel_service.py index fbb68e3..c99cb5a 100644 --- a/sdk/basyx/server/services/submodel_service.py +++ b/sdk/basyx/server/services/submodel_service.py @@ -19,6 +19,12 @@ def _jsonable_submodels(self, submodels: list[Submodel]) \ -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: return [jsonization.to_jsonable(submodel) for submodel in submodels] + def _get_submodel_by_id(self, submodel_id): + submodel = self.obj_store.get(submodel_id) + if submodel is None: + raise HTTPException(status_code=404, detail="Submodel with id " + submodel_id + " not found") + return submodel + # Endpoint specific logic def get_all_submodels_as_jsonables(self)\ -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: @@ -29,8 +35,30 @@ def add_submodel_from_body(self, json): try: self.obj_store.add(submodel) except KeyError as e: - # TODO: Provide a stacktrace) + # TODO: Provide a stacktrace # Wenn anders in Spezifikation, Stacktrace in server log raise HTTPException(status_code=400, detail=str(e)) return {"message": "Submodel processed"} + def get_submodel_jsonable_by_id(self, submodel_id: str): + submodel = self._get_submodel_by_id(submodel_id) + return jsonization.to_jsonable(submodel) + + def update_submode_by_id(self, submodel_id: str, json): + submodel = self._get_submodel_by_id(submodel_id) + new_submodel = jsonization.submodel_from_jsonable(json) + if submodel.id != new_submodel.id: + raise HTTPException(403, "Submodel with id " + submodel_id + " does not match") + # TODO: This cant be right + self.obj_store.discard(submodel) + self.obj_store.add(new_submodel) + return jsonization.to_jsonable(new_submodel) + + def delete_submodel_by_id(self, submodel_id): + submodel = self._get_submodel_by_id(submodel_id) + self.obj_store.discard(submodel) + return {"message": "Submodel with id " + submodel_id + " deleted successfully"} + + def get_submodel_elements(self, submodel_id): + pass + From 8f0a58c76cf4f07d55500a679682bd35f5b74b5a Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sat, 14 Dec 2024 11:50:12 +0100 Subject: [PATCH 03/45] sdk/basyx/server: Moved to /server Moved server package to top level Created new AASX File Server router Co-Authored-By: Simon <88947258+somsonson@users.noreply.github.com> --- {sdk/basyx/server => api}/DESIGN_NOTES.md | 0 api/pyproject.toml | 16 +++++++++++++++ {sdk/basyx => api}/server/__init__.py | 0 api/server/routes/__init__.py | 0 .../server/api => api/server/routes}/aas.py | 0 api/server/routes/aasx_file_server.py | 20 +++++++++++++++++++ .../api => api/server/routes}/submodel.py | 2 +- {sdk/basyx => api}/server/server.py | 5 +++-- api/server/services/__init__.py | 0 .../server/services/aas_service.py | 0 .../services/aasx_flie_server_service.py | 9 +++++++++ .../server/services/submodel_service.py | 0 12 files changed, 49 insertions(+), 3 deletions(-) rename {sdk/basyx/server => api}/DESIGN_NOTES.md (100%) create mode 100644 api/pyproject.toml rename {sdk/basyx => api}/server/__init__.py (100%) create mode 100644 api/server/routes/__init__.py rename {sdk/basyx/server/api => api/server/routes}/aas.py (100%) create mode 100644 api/server/routes/aasx_file_server.py rename {sdk/basyx/server/api => api/server/routes}/submodel.py (99%) rename {sdk/basyx => api}/server/server.py (74%) create mode 100644 api/server/services/__init__.py rename {sdk/basyx => api}/server/services/aas_service.py (100%) create mode 100644 api/server/services/aasx_flie_server_service.py rename {sdk/basyx => api}/server/services/submodel_service.py (100%) diff --git a/sdk/basyx/server/DESIGN_NOTES.md b/api/DESIGN_NOTES.md similarity index 100% rename from sdk/basyx/server/DESIGN_NOTES.md rename to api/DESIGN_NOTES.md diff --git a/api/pyproject.toml b/api/pyproject.toml new file mode 100644 index 0000000..60db5ef --- /dev/null +++ b/api/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "basyx-python-framework-base" +version = "0.1" +dependencies = ["aas-core3.0"] +requires-python = ">=3.8, <3.13" +authors = [ + {name = "The Eclipse BaSyx Authors"} +] + +description="The Eclipse BaSyx Python SDK, an implementation of the Asset Administration Shell for Industry 4.0 systems" +readme = "README.md" +license = {file = "./LICENSE"} diff --git a/sdk/basyx/server/__init__.py b/api/server/__init__.py similarity index 100% rename from sdk/basyx/server/__init__.py rename to api/server/__init__.py diff --git a/api/server/routes/__init__.py b/api/server/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sdk/basyx/server/api/aas.py b/api/server/routes/aas.py similarity index 100% rename from sdk/basyx/server/api/aas.py rename to api/server/routes/aas.py diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py new file mode 100644 index 0000000..0e889e6 --- /dev/null +++ b/api/server/routes/aasx_file_server.py @@ -0,0 +1,20 @@ +from typing import Any + +from aas_core3.types import Identifiable +from fastapi import APIRouter, Request, HTTPException + +from sdk.basyx import ObjectStore +from api.server.services.aasx_flie_server_service import AasxFileServerService + + +class AasxFileServerRouter: + def __init__(self, global_obj_store: ObjectStore[Identifiable]): + self.router = APIRouter() + self.obj_store = global_obj_store + self.service = AasxFileServerService(global_obj_store) + self._setup_routes() + + def _setup_routes(self): + @self.router.get("/") + async def get_submodel_all() -> Any: + return self.service.dummy() \ No newline at end of file diff --git a/sdk/basyx/server/api/submodel.py b/api/server/routes/submodel.py similarity index 99% rename from sdk/basyx/server/api/submodel.py rename to api/server/routes/submodel.py index b5d4a09..721bbbc 100644 --- a/sdk/basyx/server/api/submodel.py +++ b/api/server/routes/submodel.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Request, HTTPException from sdk.basyx import ObjectStore -from sdk.basyx.server.services.submodel_service import SubmodelService +from api.server.services.submodel_service import SubmodelService class SubmodelRouter: diff --git a/sdk/basyx/server/server.py b/api/server/server.py similarity index 74% rename from sdk/basyx/server/server.py rename to api/server/server.py index bb6222b..fcbbfeb 100644 --- a/sdk/basyx/server/server.py +++ b/api/server/server.py @@ -1,9 +1,8 @@ from fastapi import FastAPI import uvicorn -from pygments.lexers import q # Import routers -from api import submodel +from routes import submodel, aasx_file_server from sdk.basyx import object_store @@ -13,10 +12,12 @@ central_object_store = object_store.ObjectStore() submodel_router = submodel.SubmodelRouter(central_object_store) +aasx_file_router = aasx_file_server.AasxFileServerRouter(central_object_store) # Register router # TODO: This can be done dynamically based on startup params app.include_router(submodel_router.router, prefix=prefix + "/submodels") +app.include_router(aasx_file_router.router, prefix=prefix + "/aasx") # Start the server if this file is executed directly if __name__ == "__main__": diff --git a/api/server/services/__init__.py b/api/server/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sdk/basyx/server/services/aas_service.py b/api/server/services/aas_service.py similarity index 100% rename from sdk/basyx/server/services/aas_service.py rename to api/server/services/aas_service.py diff --git a/api/server/services/aasx_flie_server_service.py b/api/server/services/aasx_flie_server_service.py new file mode 100644 index 0000000..7902e2f --- /dev/null +++ b/api/server/services/aasx_flie_server_service.py @@ -0,0 +1,9 @@ +from sdk.basyx import ObjectStore + + +class AasxFileServerService(): + def __init__(self, global_object_store: ObjectStore): + self.obj_store = global_object_store + + def dummy(self): + pass \ No newline at end of file diff --git a/sdk/basyx/server/services/submodel_service.py b/api/server/services/submodel_service.py similarity index 100% rename from sdk/basyx/server/services/submodel_service.py rename to api/server/services/submodel_service.py From c6535e81ec2eb10798886fd8cd29365e02356707 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sat, 14 Dec 2024 18:48:30 +0100 Subject: [PATCH 04/45] api/README.md: Add development status as server README file Also adjusted some return value placeholders --- api/README.md | 62 +++++++++++++++++++++++++++++++++++ api/server/routes/submodel.py | 4 +-- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 api/README.md diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..267c7db --- /dev/null +++ b/api/README.md @@ -0,0 +1,62 @@ +# Server Development Status + +> [!warning] +> This README tracks the development progress of the server's endpoints. Endpoints are sorted based on the +> specification's services (Submodel, AASX File Server, ...). + +- **Implemented**: Currently available, but **not necessarily fully functional**. +- **Planned**: Scheduled for future implementation. +- **Not Planned**: Currently not scheduled for implementation. + +> [!warning] +> The project is WIP and endpoints might be declared as 'Implemented' whilst still having problems. + +Below is the status table for the endpoints, organized as specified. + +## Submodel Service +| Endpoint | Operation | Description | Status | +|--------------------------------------------------------------------------------------|-----------|-------------|-------------| +| `/submodels/` | GET | TODO | Implemented | +| `/submodels/` | POST | TODO | Planned | +| `/submodels/$metadata` | GET | TODO | Planned | +| `/submodels/$reference` | GET | TODO | Planned | +| `/submodels/$value` | GET | TODO | Not Planned | +| `/submodels/$path` | GET | TODO | Not Planned | +| `/submodels/{submodel_id}` | GET | TODO | Implemented | +| `/submodels/{submodel_id}` | PUT | TODO | Implemented | +| `/submodels/{submodel_id}` | DELETE | TODO | Implemented | +| `/submodels/{submodel_id}/$metadata` | GET | TODO | Planned | +| `/submodels/{submodel_id}/$metadata` | PATCH | TODO | Planned | +| `/submodels/{submodel_id}/$value` | GET | TODO | Not Planned | +| `/submodels/{submodel_id}/$reference` | GET | TODO | Planned | +| `/submodels/{submodel_id}/$path` | GET | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements` | GET | TODO | Implemented | +| `/submodels/{submodel_id}/submodel-elements` | POST | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/$metadata` | GET | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/$reference` | GET | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/$value` | GET | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/$path` | GET | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | GET | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | POST | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PUT | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PATCH | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | GET | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | PATCH | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$reference` | GET | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$value` | GET | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$value` | PATCH | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | GET | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | PUT | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | DELETE | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke` | POST | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke/$value` | POST | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke-async` | POST | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke-async/$value` | POST | TODO | Not Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | GET | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | POST | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | GET | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | PUT | TODO | Planned | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | DELETE | TODO | Planned | + +Tables for the remaining services will follow. \ No newline at end of file diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index 721bbbc..3143c8d 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -75,7 +75,7 @@ async def not_implemented_value_get(submodel_id: str) -> Any: @self.router.patch("/{submodel_id}/$value") async def not_implemented_value_patch(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not yet implemented!") + raise HTTPException(status_code=501, detail="This route is yet implemented!") @self.router.get("/{submodel_id}/$reference") async def get_submodels_reference(submodel_id: str) -> Any: @@ -83,7 +83,7 @@ async def get_submodels_reference(submodel_id: str) -> Any: @self.router.get("/{submodel_id}/$path") async def not_implemented_path_get(submodel_id: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not yet implemented!") + raise HTTPException(status_code=501, detail="This route is yet implemented!") @self.router.get("/{submodel_id}/submodel-elements") async def get_submodel_submodel_elements(submodel_id: str) -> Any: From 7c56034f8b71c1532cc23c8d1f750a0a1886c4e0 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Mon, 16 Dec 2024 14:43:45 +0100 Subject: [PATCH 05/45] api/pyproject.toml: Update pyproject to contain required dependencies --- api/pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 60db5ef..00987ce 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -3,14 +3,14 @@ requires = ["setuptools >= 61.0"] build-backend = "setuptools.build_meta" [project] -name = "basyx-python-framework-base" +name = "basyx-server" # TODO: name is tbd version = "0.1" -dependencies = ["aas-core3.0"] +dependencies = ["aas-core3.0","fastapi","basyx-python-framework-base","uvicorn"] requires-python = ">=3.8, <3.13" authors = [ {name = "The Eclipse BaSyx Authors"} ] - -description="The Eclipse BaSyx Python SDK, an implementation of the Asset Administration Shell for Industry 4.0 systems" +# TODO: description is tbd +description="The Eclipse BaSyx Server does stuff" readme = "README.md" license = {file = "./LICENSE"} From c923af75eda77a2fab5d2a9eca2d8aeaf243bf5f Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Tue, 17 Dec 2024 12:10:29 +0100 Subject: [PATCH 06/45] update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 09768ea..e7fe581 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /venv/ /sdk/venv sdk/basyx_python_framework.egg-info/ +api/basyx_server.egg-info/ # IDE settings /.idea/ From cad1886ed61159dc13dedfe521248b5e86e87085 Mon Sep 17 00:00:00 2001 From: Simon Suchan Date: Tue, 17 Dec 2024 12:17:23 +0100 Subject: [PATCH 07/45] api/server: fix python imports and codestyle --- api/server/routes/aasx_file_server.py | 6 +++--- api/server/routes/submodel.py | 4 ++-- api/server/server.py | 3 +-- api/server/services/aasx_flie_server_service.py | 4 ++-- api/server/services/submodel_service.py | 3 +-- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index 0e889e6..b5b0468 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -3,8 +3,8 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request, HTTPException -from sdk.basyx import ObjectStore -from api.server.services.aasx_flie_server_service import AasxFileServerService +from basyx import ObjectStore +from services.aasx_flie_server_service import AasxFileServerService class AasxFileServerRouter: @@ -17,4 +17,4 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]): def _setup_routes(self): @self.router.get("/") async def get_submodel_all() -> Any: - return self.service.dummy() \ No newline at end of file + return self.service.dummy() diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index 3143c8d..3365508 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -3,8 +3,8 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request, HTTPException -from sdk.basyx import ObjectStore -from api.server.services.submodel_service import SubmodelService +from basyx import ObjectStore +from services.submodel_service import SubmodelService class SubmodelRouter: diff --git a/api/server/server.py b/api/server/server.py index fcbbfeb..39fb357 100644 --- a/api/server/server.py +++ b/api/server/server.py @@ -4,7 +4,7 @@ # Import routers from routes import submodel, aasx_file_server -from sdk.basyx import object_store +from basyx import object_store app = FastAPI() prefix = "/api/v3.0" @@ -17,7 +17,6 @@ # Register router # TODO: This can be done dynamically based on startup params app.include_router(submodel_router.router, prefix=prefix + "/submodels") -app.include_router(aasx_file_router.router, prefix=prefix + "/aasx") # Start the server if this file is executed directly if __name__ == "__main__": diff --git a/api/server/services/aasx_flie_server_service.py b/api/server/services/aasx_flie_server_service.py index 7902e2f..d584f3a 100644 --- a/api/server/services/aasx_flie_server_service.py +++ b/api/server/services/aasx_flie_server_service.py @@ -1,4 +1,4 @@ -from sdk.basyx import ObjectStore +from basyx import ObjectStore class AasxFileServerService(): @@ -6,4 +6,4 @@ def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store def dummy(self): - pass \ No newline at end of file + pass diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index c99cb5a..e1392f0 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -4,7 +4,7 @@ from aas_core3.types import Submodel from fastapi import HTTPException -from sdk.basyx import ObjectStore +from basyx import ObjectStore class SubmodelService: @@ -61,4 +61,3 @@ def delete_submodel_by_id(self, submodel_id): def get_submodel_elements(self, submodel_id): pass - From cba17239d9ee63b0f337a710cec59d0bb950acf2 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Fri, 3 Jan 2025 11:32:43 +0100 Subject: [PATCH 08/45] object_store: Add delete function to delete by id and not only by instance --- sdk/basyx/object_store.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sdk/basyx/object_store.py b/sdk/basyx/object_store.py index 73bc68d..fe33d11 100644 --- a/sdk/basyx/object_store.py +++ b/sdk/basyx/object_store.py @@ -131,6 +131,15 @@ def discard(self, x: _IdentifiableType) -> None: if self._backend.get(x.id) is x: del self._backend[x.id] + def delete(self, x: str) -> None: + """ + Discard identifiable from the Objectstore + + :param x: Id of Identifiable instance to discard + """ + if self._backend.get(x): + del self._backend[x] + def get_referable(self, identifier: str, id_short: str) -> Referable: """ Get referable by using its id_short and the identifier of the identifiable it refers to From 308ddb64dbc5f157c74aae7662f363965eae9b29 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Fri, 3 Jan 2025 11:33:34 +0100 Subject: [PATCH 09/45] api/server/aasx: Add AASX File Server Interface --- api/server/routes/aasx_file_server.py | 23 +++++++++- api/server/server.py | 1 + .../services/aasx_flie_server_service.py | 46 +++++++++++++++++-- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index b5b0468..69e4c39 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -16,5 +16,24 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]): def _setup_routes(self): @self.router.get("/") - async def get_submodel_all() -> Any: - return self.service.dummy() + async def GetAllAASXPackageIds() -> Any: + return self.service.GetAllAASXPackageIds() + + @self.router.get("/{aasx_package_id}") + async def GetAASXByPackageId(aasx_package_id: str) -> Any: + return self.service.GetAASXByPackageId(aasx_package_id) + + @self.router.post("/") + async def PostAASXPackage(request: Request) -> Any: + body = await request.json() + return self.service.PostAASXPackage(body) + + @self.router.put("/") + async def PutAASXByPackageId(request: Request) -> Any: + body = await request.json() + return self.service.PutAASXByPackageId(body) + + @self.router.delete("/{aasx_package_id}") + async def DeleteAASXByPackageId(aasx_package_id: str) -> Any: + return self.service.DeleteAASXByPackageId(aasx_package_id) + \ No newline at end of file diff --git a/api/server/server.py b/api/server/server.py index 39fb357..25bff0e 100644 --- a/api/server/server.py +++ b/api/server/server.py @@ -17,6 +17,7 @@ # Register router # TODO: This can be done dynamically based on startup params app.include_router(submodel_router.router, prefix=prefix + "/submodels") +app.include_router(aasx_file_router.router, prefix=prefix + "/aasx") # Start the server if this file is executed directly if __name__ == "__main__": diff --git a/api/server/services/aasx_flie_server_service.py b/api/server/services/aasx_flie_server_service.py index d584f3a..27c98d7 100644 --- a/api/server/services/aasx_flie_server_service.py +++ b/api/server/services/aasx_flie_server_service.py @@ -1,9 +1,49 @@ from basyx import ObjectStore +from aas_core3.types import AssetAdministrationShell +from aas_core3 import jsonization +from fastapi import HTTPException +from typing import Any, MutableMapping -class AasxFileServerService(): +class AasxFileServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def dummy(self): - pass + def GetAllAASXPackageIds(self) -> list[str]: + return [item.id for item in self.obj_store if isinstance(item, AssetAdministrationShell)] + + def GetAASXByPackageId(self, package_id) \ + -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + aasx_package = self.obj_store.get_identifiable(package_id) + assert isinstance(aasx_package, AssetAdministrationShell) + return jsonization.to_jsonable(aasx_package) + + def PostAASXPackage(self, json): + aasx_package = jsonization.asset_administration_shell_from_jsonable(json) + try: + self.obj_store.add(aasx_package) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "AASX package processed"} + + def PutAASXByPackageId(self, json): + aasx_package = jsonization.asset_administration_shell_from_jsonable(json) + try: + self.obj_store.delete(aasx_package.id) # should there be an exception if there is no aasx_package to update? + self.obj_store.add(aasx_package) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "AASX package updated"} + + def DeleteAASXByPackageId(self, package_id): + try: + self.obj_store.delete(package_id) # should there be an exception if there is no aasx_package to update? + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "AASX package deleted"} From 6ffa7d8bff0ca77ac879faada6139039c262a386 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Fri, 3 Jan 2025 16:13:25 +0100 Subject: [PATCH 10/45] api/README.md: AASX File Server Interface and Operations specified in README --- api/README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/README.md b/api/README.md index 267c7db..a301fed 100644 --- a/api/README.md +++ b/api/README.md @@ -59,4 +59,14 @@ Below is the status table for the endpoints, organized as specified. | `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | PUT | TODO | Planned | | `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | DELETE | TODO | Planned | -Tables for the remaining services will follow. \ No newline at end of file +Tables for the remaining services will follow. + + +## AASX File Server Interface and Operations +| Endpoint | Operation | Description | Status | +|---------------------------|-----------|-------------|---------| +| `/GetAllAASXPackageIds/` | GET | TODO | Planned | +| `/GetAASXByPackageId/` | POST | TODO | Planned | +| `/PostAASXPackage/` | POST | TODO | Planned | +| `/PutAASXByPackageId/` | PUT | TODO | Planned | +| `/DeleteAASXByPackageId/` | DELETE | TODO | Planned | From 675f7881eceda232adff16917d6b9478a6f10bb1 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Mon, 6 Jan 2025 13:48:23 +0100 Subject: [PATCH 11/45] api/README.md: Update formatting and add all endpoints --- api/README.md | 180 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 123 insertions(+), 57 deletions(-) diff --git a/api/README.md b/api/README.md index a301fed..e3c43f4 100644 --- a/api/README.md +++ b/api/README.md @@ -4,69 +4,135 @@ > This README tracks the development progress of the server's endpoints. Endpoints are sorted based on the > specification's services (Submodel, AASX File Server, ...). -- **Implemented**: Currently available, but **not necessarily fully functional**. -- **Planned**: Scheduled for future implementation. -- **Not Planned**: Currently not scheduled for implementation. +- **Implemented (✅)**: Currently available, but **not necessarily fully functional**. +- **Planned (📅)**: Scheduled for future implementation. +- **Not Planned (❌)**: Currently not scheduled for implementation. > [!warning] -> The project is WIP and endpoints might be declared as 'Implemented' whilst still having problems. +> The project is WIP and endpoints might be declared as 'Implemented' whilst still having issues. Below is the status table for the endpoints, organized as specified. -## Submodel Service -| Endpoint | Operation | Description | Status | -|--------------------------------------------------------------------------------------|-----------|-------------|-------------| -| `/submodels/` | GET | TODO | Implemented | -| `/submodels/` | POST | TODO | Planned | -| `/submodels/$metadata` | GET | TODO | Planned | -| `/submodels/$reference` | GET | TODO | Planned | -| `/submodels/$value` | GET | TODO | Not Planned | -| `/submodels/$path` | GET | TODO | Not Planned | -| `/submodels/{submodel_id}` | GET | TODO | Implemented | -| `/submodels/{submodel_id}` | PUT | TODO | Implemented | -| `/submodels/{submodel_id}` | DELETE | TODO | Implemented | -| `/submodels/{submodel_id}/$metadata` | GET | TODO | Planned | -| `/submodels/{submodel_id}/$metadata` | PATCH | TODO | Planned | -| `/submodels/{submodel_id}/$value` | GET | TODO | Not Planned | -| `/submodels/{submodel_id}/$reference` | GET | TODO | Planned | -| `/submodels/{submodel_id}/$path` | GET | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements` | GET | TODO | Implemented | -| `/submodels/{submodel_id}/submodel-elements` | POST | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/$metadata` | GET | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/$reference` | GET | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/$value` | GET | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/$path` | GET | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | GET | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | POST | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PUT | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PATCH | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | GET | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | PATCH | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$reference` | GET | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$value` | GET | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$value` | PATCH | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | GET | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | PUT | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | DELETE | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke` | POST | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke/$value` | POST | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke-async` | POST | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke-async/$value` | POST | TODO | Not Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | GET | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | POST | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | GET | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | PUT | TODO | Planned | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | DELETE | TODO | Planned | +## AAS Service +| Endpoint | Operation | Description | Status | +|------------------------------------|-----------|-------------------------------------------------|--------| +| `/aas` | GET | Returns the Asset Administration Shell | 📅 | +| `/aas` | PUT | Replaces the current Asset Administration Shell | 📅 | +| `/aas/submodel-refs` | GET | Returns all Submodel References | 📅 | +| `/aas/submodel-refs` | POST | Creates a Submodel Reference | 📅 | +| `/aas/submodel-refs/{submodel_id}` | DELETE | Deletes a Submodel Reference | 📅 | +| `/aas/asset-information` | GET | Returns the Asset Information | 📅 | +| `/aas/asset-information` | PUT | Replaces the Asset Information | 📅 | +| `/aas/asset-information/thumbnail` | GET | Returns the thumbnail file | 📅 | +| `/aas/asset-information/thumbnail` | PUT | Replaces the thumbnail file | 📅 | +| `/aas/asset-information/thumbnail` | DELETE | Deletes the thumbnail file | 📅 | -Tables for the remaining services will follow. +## Submodel Service +| Endpoint | Operation | Description | Status | +|--------------------------------------------------------------------------------------|-----------|------------------------------------------------------------------------------|--------| +| `/submodels/` | GET | Retrieve all submodels | ✅ | +| `/submodels/` | POST | Create a new submodel | ✅ | +| `/submodels/$metadata` | GET | Retrieve metadata for all submodels | 📅 | +| `/submodels/$reference` | GET | Retrieve reference for all submodels | 📅 | +| `/submodels/$value` | GET | Retrieve values of all submodels | ❌ | +| `/submodels/$path` | GET | Retrieve submodels by a specific path | ❌ | +| `/submodels/{submodel_id}` | GET | Retrieve a submodel by ID | ✅ | +| `/submodels/{submodel_id}` | PUT | Update a submodel by ID | ✅ | +| `/submodels/{submodel_id}` | DELETE | Delete a submodel by ID | ✅ | +| `/submodels/{submodel_id}/$metadata` | GET | Retrieve metadata of a specific submodel | 📅 | +| `/submodels/{submodel_id}/$metadata` | PATCH | Update metadata of a specific submodel | 📅 | +| `/submodels/{submodel_id}/$value` | GET | Retrieve values of a specific submodel | ❌ | +| `/submodels/{submodel_id}/$reference` | GET | Retrieve reference of a specific submodel | 📅 | +| `/submodels/{submodel_id}/$path` | GET | Retrieve a specific submodel by path | ❌ | +| `/submodels/{submodel_id}/submodel-elements` | GET | Retrieve all elements of a specific submodel | ✅ | +| `/submodels/{submodel_id}/submodel-elements` | POST | Create new elements in a specific submodel | 📅 | +| `/submodels/{submodel_id}/submodel-elements/$metadata` | GET | Retrieve metadata for submodel elements | 📅 | +| `/submodels/{submodel_id}/submodel-elements/$reference` | GET | Retrieve references for submodel elements | 📅 | +| `/submodels/{submodel_id}/submodel-elements/$value` | GET | Retrieve values for submodel elements | ❌ | +| `/submodels/{submodel_id}/submodel-elements/$path` | GET | Retrieve elements by path in a specific submodel | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | GET | Retrieve specific elements by short ID in a submodel | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | POST | Create specific elements by short ID in a submodel | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PUT | Update specific elements by short ID in a submodel | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | Delete specific elements by short ID in a submodel | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PATCH | Partially update specific elements by short ID in a submodel | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | GET | Retrieve metadata of specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | PATCH | Update metadata of specific elements by short ID | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$reference` | GET | Retrieve reference of specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$value` | GET | Retrieve values of specific elements by short ID | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$value` | PATCH | Update values of specific elements by short ID | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | GET | Retrieve attachments of specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | PUT | Update attachments of specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | DELETE | Delete attachments of specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke` | POST | Invoke operations on specific elements by short ID | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke/$value` | POST | Invoke operations with value on specific elements by short ID | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke-async` | POST | Asynchronously invoke operations on specific elements by short ID | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke-async/$value` | POST | Asynchronously invoke operations with value on specific elements by short ID | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | GET | Retrieve qualifiers for specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | POST | Add qualifiers to specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | GET | Retrieve qualifiers of a specific type for specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | PUT | Update qualifiers of a specific type for specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | DELETE | Delete qualifiers of a specific type for specific elements by short ID | 📅 | ## AASX File Server Interface and Operations -| Endpoint | Operation | Description | Status | -|---------------------------|-----------|-------------|---------| -| `/GetAllAASXPackageIds/` | GET | TODO | Planned | -| `/GetAASXByPackageId/` | POST | TODO | Planned | -| `/PostAASXPackage/` | POST | TODO | Planned | -| `/PutAASXByPackageId/` | PUT | TODO | Planned | -| `/DeleteAASXByPackageId/` | DELETE | TODO | Planned | +| Endpoint | Operation | Description | Status | +|---------------------------|-----------|-------------|--------| +| `/GetAllAASXPackageIds/` | GET | TODO | 📅 | +| `/GetAASXByPackageId/` | POST | TODO | 📅 | +| `/PostAASXPackage/` | POST | TODO | 📅 | +| `/PutAASXByPackageId/` | PUT | TODO | 📅 | +| `/DeleteAASXByPackageId/` | DELETE | TODO | 📅 | + +## AAS Registry Service +| Endpoint | Operation | Description | Status | +|--------------------------------------|-----------|----------------------------------------------------|--------| +| `/registry/aas-descriptors` | GET | Returns all Asset Administration Shell Descriptors | 📅 | +| `/registry/aas-descriptors/{aas_id}` | GET | Returns an AAS Descriptor by ID | 📅 | +| `/registry/aas-descriptors` | POST | Creates an AAS Descriptor | 📅 | +| `/registry/aas-descriptors/{aas_id}` | PUT | Updates an AAS Descriptor | 📅 | +| `/registry/aas-descriptors/{aas_id}` | DELETE | Deletes an AAS Descriptor | 📅 | + +## Submodel Registry Service +| Endpoint | Operation | Description | Status | +|------------------------------------------------|-----------|-------------------------------------|--------| +| `/registry/submodel-descriptors` | GET | Returns all Submodel Descriptors | 📅 | +| `/registry/submodel-descriptors/{submodel_id}` | GET | Returns a Submodel Descriptor by ID | 📅 | +| `/registry/submodel-descriptors` | POST | Creates a Submodel Descriptor | 📅 | +| `/registry/submodel-descriptors/{submodel_id}` | PUT | Updates a Submodel Descriptor | 📅 | +| `/registry/submodel-descriptors/{submodel_id}` | DELETE | Deletes a Submodel Descriptor | 📅 | + +## Discovery Service +| Endpoint | Operation | Description | Status | +|-----------------------------------|-----------|--------------------------------------|--------| +| `/discovery/aas-ids` | GET | Returns all AAS IDs by an Asset Link | 📅 | +| `/discovery/asset-links/{aas_id}` | GET | Returns all Asset Links by an AAS ID | 📅 | +| `/discovery/asset-links/{aas_id}` | POST | Posts new Asset Links | 📅 | +| `/discovery/asset-links/{aas_id}` | DELETE | Deletes Asset Links | 📅 | + +## AAS Repository Service +| Endpoint | Operation | Description | Status | +|--------------------------------------------|-----------|---------------------------------------------|--------| +| `/shells` | GET | Returns all Asset Administration Shells | 📅 | +| `/shells/{aas_id}` | GET | Returns an Asset Administration Shell by ID | 📅 | +| `/shells/{aas_id}` | PUT | Updates an Asset Administration Shell by ID | 📅 | +| `/shells/{aas_id}` | DELETE | Deletes an Asset Administration Shell by ID | 📅 | +| `/shells/{aas_id}/asset-information` | GET | Returns Asset Information for an AAS | 📅 | +| `/shells/{aas_id}/submodels` | GET | Returns all Submodels for an AAS | 📅 | +| `/shells/{aas_id}/submodels/{submodel_id}` | GET | Returns a Submodel by ID for an AAS | 📅 | + +## Submodel Repository Service +| Endpoint | Operation | Description | Status | +|----------------------------|-----------|--------------------------|--------| +| `/submodels` | GET | Returns all Submodels | 📅 | +| `/submodels/{submodel_id}` | GET | Returns a Submodel by ID | 📅 | +| `/submodels/{submodel_id}` | PUT | Updates a Submodel by ID | 📅 | +| `/submodels/{submodel_id}` | DELETE | Deletes a Submodel by ID | 📅 | + +## ConceptDescription Repository Service +| Endpoint | Operation | Description | Status | +|--------------------------------------|-----------|-------------------------------------|--------| +| `/concept-descriptions` | GET | Returns all Concept Descriptions | 📅 | +| `/concept-descriptions/{concept_id}` | GET | Returns a Concept Description by ID | 📅 | +| `/concept-descriptions/{concept_id}` | POST | Creates a new Concept Description | 📅 | +| `/concept-descriptions/{concept_id}` | PUT | Updates a Concept Description by ID | 📅 | +| `/concept-descriptions/{concept_id}` | DELETE | Deletes a Concept Description by ID | 📅 | \ No newline at end of file From b9a9668e4f4ceedf319e8e13246c85c794c5d631 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Tue, 14 Jan 2025 12:39:18 +0100 Subject: [PATCH 12/45] api/server: Add aas registry server and submodel registry server to server. sdk/basyx/object_store: add filter function --- api/server/routes/aas_registry_server.py | 38 ++++++ api/server/routes/submodel_registry_server.py | 38 ++++++ api/server/server.py | 6 +- .../services/aas_registry_server_service.py | 74 ++++++++++++ .../services/aasx_flie_server_service.py | 5 +- .../submodel_registry_server_service.py | 113 ++++++++++++++++++ sdk/basyx/object_store.py | 15 ++- 7 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 api/server/routes/aas_registry_server.py create mode 100644 api/server/routes/submodel_registry_server.py create mode 100644 api/server/services/aas_registry_server_service.py create mode 100644 api/server/services/submodel_registry_server_service.py diff --git a/api/server/routes/aas_registry_server.py b/api/server/routes/aas_registry_server.py new file mode 100644 index 0000000..647aef4 --- /dev/null +++ b/api/server/routes/aas_registry_server.py @@ -0,0 +1,38 @@ +from typing import Any + +from aas_core3.types import Identifiable +from fastapi import APIRouter, Request, HTTPException + +from basyx import ObjectStore +from services.aas_registry_server_service import AasRegistryServerService + + +class AasRegistryRouter: + def __init__(self, global_obj_store: ObjectStore[Identifiable]): + self.router = APIRouter() + self.obj_store = global_obj_store + self.service = AasRegistryServerService(global_obj_store) + self._setup_routes() + + def _setup_routes(self): + @self.router.get("/") + async def GetAllAssetAdministrationShellDescriptors() -> Any: + return self.service.GetAllAssetAdministrationShellDescriptors() + + @self.router.get("/{aas_descriptor_id}") + async def GetAssetAdministrationShellDescriptorById(aas_descriptor_id: str) -> Any: + return self.service.GetAssetAdministrationShellDescriptorById(aas_descriptor_id) + + @self.router.post("/") + async def PostAssetAdministrationShellDescriptor(request: Request) -> Any: + body = await request.json() + return self.service.PostAssetAdministrationShellDescriptor(body) + + @self.router.put("/") + async def PutAssetAdministrationShellDescriptorById(request: Request) -> Any: + body = await request.json() + return self.service.PutAssetAdministrationShellDescriptorById(body) + + @self.router.delete("/{aas_descriptor_id}") + async def DeleteAssetAdministrationShellDescriptorById(aas_descriptor_id: str) -> Any: + return self.service.DeleteAssetAdministrationShellDescriptorById(aas_descriptor_id) diff --git a/api/server/routes/submodel_registry_server.py b/api/server/routes/submodel_registry_server.py new file mode 100644 index 0000000..c9b39b6 --- /dev/null +++ b/api/server/routes/submodel_registry_server.py @@ -0,0 +1,38 @@ +from typing import Any + +from aas_core3.types import Identifiable +from fastapi import APIRouter, Request, HTTPException + +from basyx import ObjectStore +from services.submodel_registry_server_service import SubmodelRegistryServerService + + +class SubmodelRegistryRouter: + def __init__(self, global_obj_store: ObjectStore[Identifiable]): + self.router = APIRouter() + self.obj_store = global_obj_store + self.service = SubmodelRegistryServerService(global_obj_store) + self._setup_routes() + + def _setup_routes(self): + @self.router.get("/") + async def GetAllSubmodelDescriptors() -> Any: + return self.service.GetAllSubmodelDescriptors() + + @self.router.get("/{submodel_id}") + async def GetSubmodelDescriptorById(submodel_id: str) -> Any: + return self.service.GetSubmodelDescriptorById(submodel_id) + + @self.router.post("/") + async def PostSubmodelDescriptor(request: Request) -> Any: + body = await request.json() + return self.service.PostSubmodelDescriptor(body) + + @self.router.put("/") + async def PutSubmodelDescriptorById(request: Request) -> Any: + body = await request.json() + return self.service.PutSubmodelDescriptorById(body) + + @self.router.delete("/{submodel_id}") + async def DeleteSubmodelDescriptorById(submodel_id: str) -> Any: + return self.service.DeleteSubmodelDescriptorById(submodel_id) diff --git a/api/server/server.py b/api/server/server.py index 25bff0e..0e15fb6 100644 --- a/api/server/server.py +++ b/api/server/server.py @@ -2,7 +2,7 @@ import uvicorn # Import routers -from routes import submodel, aasx_file_server +from routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server from basyx import object_store @@ -13,11 +13,15 @@ submodel_router = submodel.SubmodelRouter(central_object_store) aasx_file_router = aasx_file_server.AasxFileServerRouter(central_object_store) +aas_registry_router = aas_registry_server.AasRegistryRouter(central_object_store) +submodel_registry_router = submodel_registry_server.SubmodelRegistryRouter(central_object_store) # Register router # TODO: This can be done dynamically based on startup params app.include_router(submodel_router.router, prefix=prefix + "/submodels") app.include_router(aasx_file_router.router, prefix=prefix + "/aasx") +app.include_router(aas_registry_router.router, prefix=prefix + "/aas_registry") +app.include_router(submodel_registry_router.router, prefix=prefix + "/submodel_registry") # Start the server if this file is executed directly if __name__ == "__main__": diff --git a/api/server/services/aas_registry_server_service.py b/api/server/services/aas_registry_server_service.py new file mode 100644 index 0000000..0bb2028 --- /dev/null +++ b/api/server/services/aas_registry_server_service.py @@ -0,0 +1,74 @@ +from basyx import ObjectStore +from aas_core3.types import AssetAdministrationShell, ConceptDescription +from aas_core3 import jsonization +from fastapi import HTTPException +from typing import Any, MutableMapping +from aas_core3 import jsonization + + +class AasRegistryServerService: + def __init__(self, global_object_store: ObjectStore): + self.obj_store = global_object_store + + def GetAllAssetAdministrationShellDescriptors(self) -> list[str]: + all_descriptors = self.obj_store.filter_identifiables_by_instance(ConceptDescription) + print(all_descriptors.__dict__) + print(all_descriptors) + print("test") + aas_descriptors_store = ObjectStore() + for descriptor in all_descriptors: + reference_list = descriptor.is_case_of + print(reference_list) + for element in reference_list: + reference_ids = element.keys + for reference_id in reference_ids: + try: + identifiable = self.obj_store.get_identifiable(reference_id) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + + if isinstance(identifiable, AssetAdministrationShell): + try: + aas_descriptors_store.add(descriptor) + except KeyError as e: + pass + return [jsonization.to_jsonable(descriptor) for descriptor in aas_descriptors_store] + + def GetAssetAdministrationShellDescriptorById(self, descriptor_id) \ + -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + aas_descriptor = self.obj_store.get_identifiable(descriptor_id) + assert isinstance(aas_descriptor, ConceptDescription) + return jsonization.to_jsonable(aas_descriptor) + + def PostAssetAdministrationShellDescriptor(self, json): + aas_descriptor = jsonization.concept_description_from_jsonable(json) + try: + self.obj_store.add(aas_descriptor) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "AAS Descriptor processed"} + + def PutAssetAdministrationShellDescriptorById(self, json): + aas_descriptor = jsonization.asset_administration_shell_from_jsonable(json) + try: + self.obj_store.delete(aas_descriptor.id) # should there be an exception if there is no aasx_package to + # update? + self.obj_store.add(aas_descriptor) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "AASX package updated"} + + def DeleteAssetAdministrationShellDescriptorById(self, descriptor_id): + try: + self.obj_store.delete(descriptor_id) # should there be an exception if there is no aasx_package to delete? + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "AASX descriptor deleted"} diff --git a/api/server/services/aasx_flie_server_service.py b/api/server/services/aasx_flie_server_service.py index 27c98d7..b1abd9c 100644 --- a/api/server/services/aasx_flie_server_service.py +++ b/api/server/services/aasx_flie_server_service.py @@ -31,7 +31,8 @@ def PostAASXPackage(self, json): def PutAASXByPackageId(self, json): aasx_package = jsonization.asset_administration_shell_from_jsonable(json) try: - self.obj_store.delete(aasx_package.id) # should there be an exception if there is no aasx_package to update? + self.obj_store.delete(aasx_package.id) # should there be an exception if there is no aasx_package to + # update? self.obj_store.add(aasx_package) except KeyError as e: # TODO: Provide a stacktrace @@ -41,7 +42,7 @@ def PutAASXByPackageId(self, json): def DeleteAASXByPackageId(self, package_id): try: - self.obj_store.delete(package_id) # should there be an exception if there is no aasx_package to update? + self.obj_store.delete(package_id) # should there be an exception if there is no aasx_package to delete? except KeyError as e: # TODO: Provide a stacktrace # Wenn anders in Spezifikation, Stacktrace in server log diff --git a/api/server/services/submodel_registry_server_service.py b/api/server/services/submodel_registry_server_service.py new file mode 100644 index 0000000..e43c3a0 --- /dev/null +++ b/api/server/services/submodel_registry_server_service.py @@ -0,0 +1,113 @@ +from basyx import ObjectStore +from aas_core3.types import AssetAdministrationShell, Submodel +from aas_core3 import jsonization +from fastapi import HTTPException +from typing import Any, MutableMapping +from aas_core3.types import AssetAdministrationShell, ConceptDescription + + +class SubmodelRegistryServerService: + def __init__(self, global_object_store: ObjectStore): + self.obj_store = global_object_store + + def GetAllSubmodelDescriptors(self) -> list[str]: + #print(self.obj_store.__dict__) + + all_descriptors = self.obj_store.filter_identifiables_by_instance(ConceptDescription) + #print(all_descriptors.__dict__) + print(all_descriptors) + #print("test") + submodel_descriptors_store = ObjectStore() + for descriptor in all_descriptors: + reference_list = descriptor.is_case_of + print(reference_list) + for element in reference_list: + reference_ids = element.keys + print(reference_ids) + for reference_id in reference_ids: + try: + identifiable = self.obj_store.get_identifiable(reference_id.value) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + identifiable = [] + if isinstance(identifiable, Submodel): + try: + submodel_descriptors_store.add(descriptor) + except KeyError as e: + pass + return [jsonization.to_jsonable(descriptor) for descriptor in submodel_descriptors_store] + + def GetSubmodelDescriptorById(self, descriptor_id) \ + -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + try: + aas_descriptor = self.obj_store.get_identifiable(descriptor_id) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + assert isinstance(aas_descriptor, ConceptDescription) + return jsonization.to_jsonable(aas_descriptor) + + def PostSubmodelDescriptor(self, json): + submodel_descriptor = jsonization.concept_description_from_jsonable(json) + + # Check if all referenced submodels exist in the obeject_store + + for reference in submodel_descriptor.is_case_of: + reference_ids = reference.keys + for reference_id in reference_ids: + try: + self.obj_store.get_identifiable(reference_id.value) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail= "A referenced submodel of the concept description " + "with the following id does not exist in the " + "object_store:" + str(e)) + + try: + self.obj_store.add(submodel_descriptor) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail="A referenced submodel of the concept description " + "with the following id does not exist in the " + "object_store:" + str(e)) + return {"message": "Submodel descriptor processed"} + + def PutSubmodelDescriptorById(self, json): + submodel_descriptor = jsonization.concept_description_from_jsonable(json) + + # Check if all referenced submodels exist in the obeject_store + + for reference in submodel_descriptor.is_case_of: + reference_ids = reference.keys + for reference_id in reference_ids: + try: + self.obj_store.get_identifiable(reference_id.value) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail= "A referenced submodel of the concept description " + "with the following id does not exist in the " + "object_store:" + str(e)) + + try: + self.obj_store.delete(submodel_descriptor.id) # should there be an exception if there is no aasx_package to + # update? + self.obj_store.add(submodel_descriptor) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "AASX package updated"} + + def DeleteSubmodelDescriptorById(self, descriptor_id): + try: + self.obj_store.delete(descriptor_id) # should there be an exception if there is no aasx_package to delete? + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "Submodel descriptor deleted"} diff --git a/sdk/basyx/object_store.py b/sdk/basyx/object_store.py index fe33d11..090b486 100644 --- a/sdk/basyx/object_store.py +++ b/sdk/basyx/object_store.py @@ -10,7 +10,7 @@ """ import abc -from typing import MutableSet, Iterator, Generic, TypeVar, Dict, List, Optional, Iterable +from typing import MutableSet, Iterator, Generic, TypeVar, Dict, List, Optional, Iterable, Type from aas_core3.types import Identifiable, Referable, Class @@ -190,6 +190,19 @@ def get_parent_referable(self, id_short: str) -> Referable: return element raise KeyError("there is no parent Identifiable for id_short {}".format(id_short)) + def filter_identifiables_by_instance(self, instance: Type) -> list[Type]: + """ + Get all identifiables of the specified type. + + :param instance: The Type to filter by. For example, we can filter by "aas_core3.types.ConceptDescription" + :return: The list of the filtered identifiables + """ + filtered_identifiables = [] + for identifiable in self._backend.values(): + if isinstance(identifiable, instance): + filtered_identifiables.append(identifiable) + return filtered_identifiables + def __contains__(self, x: object) -> bool: if isinstance(x, str): return x in self._backend From 0a396083c84ed31cfc43c04bff003be909cadb7b Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Tue, 14 Jan 2025 12:48:24 +0100 Subject: [PATCH 13/45] uptdate readme.md --- api/README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/api/README.md b/api/README.md index e3c43f4..fb49169 100644 --- a/api/README.md +++ b/api/README.md @@ -77,29 +77,29 @@ Below is the status table for the endpoints, organized as specified. ## AASX File Server Interface and Operations | Endpoint | Operation | Description | Status | |---------------------------|-----------|-------------|--------| -| `/GetAllAASXPackageIds/` | GET | TODO | 📅 | -| `/GetAASXByPackageId/` | POST | TODO | 📅 | -| `/PostAASXPackage/` | POST | TODO | 📅 | -| `/PutAASXByPackageId/` | PUT | TODO | 📅 | -| `/DeleteAASXByPackageId/` | DELETE | TODO | 📅 | +| `/GetAllAASXPackageIds/` | GET | TODO | ✅ | +| `/GetAASXByPackageId/` | POST | TODO | ✅ | +| `/PostAASXPackage/` | POST | TODO | ✅ | +| `/PutAASXByPackageId/` | PUT | TODO | ✅ | +| `/DeleteAASXByPackageId/` | DELETE | TODO | ✅ | ## AAS Registry Service | Endpoint | Operation | Description | Status | |--------------------------------------|-----------|----------------------------------------------------|--------| -| `/registry/aas-descriptors` | GET | Returns all Asset Administration Shell Descriptors | 📅 | -| `/registry/aas-descriptors/{aas_id}` | GET | Returns an AAS Descriptor by ID | 📅 | -| `/registry/aas-descriptors` | POST | Creates an AAS Descriptor | 📅 | -| `/registry/aas-descriptors/{aas_id}` | PUT | Updates an AAS Descriptor | 📅 | -| `/registry/aas-descriptors/{aas_id}` | DELETE | Deletes an AAS Descriptor | 📅 | +| `/registry/aas-descriptors` | GET | Returns all Asset Administration Shell Descriptors | ✅ | +| `/registry/aas-descriptors/{aas_id}` | GET | Returns an AAS Descriptor by ID | ✅ | +| `/registry/aas-descriptors` | POST | Creates an AAS Descriptor | ✅ | +| `/registry/aas-descriptors/{aas_id}` | PUT | Updates an AAS Descriptor | ✅ | +| `/registry/aas-descriptors/{aas_id}` | DELETE | Deletes an AAS Descriptor | ✅ | ## Submodel Registry Service | Endpoint | Operation | Description | Status | |------------------------------------------------|-----------|-------------------------------------|--------| -| `/registry/submodel-descriptors` | GET | Returns all Submodel Descriptors | 📅 | -| `/registry/submodel-descriptors/{submodel_id}` | GET | Returns a Submodel Descriptor by ID | 📅 | -| `/registry/submodel-descriptors` | POST | Creates a Submodel Descriptor | 📅 | -| `/registry/submodel-descriptors/{submodel_id}` | PUT | Updates a Submodel Descriptor | 📅 | -| `/registry/submodel-descriptors/{submodel_id}` | DELETE | Deletes a Submodel Descriptor | 📅 | +| `/registry/submodel-descriptors` | GET | Returns all Submodel Descriptors | ✅ | +| `/registry/submodel-descriptors/{submodel_id}` | GET | Returns a Submodel Descriptor by ID | ✅ | +| `/registry/submodel-descriptors` | POST | Creates a Submodel Descriptor | ✅ | +| `/registry/submodel-descriptors/{submodel_id}` | PUT | Updates a Submodel Descriptor | ✅ | +| `/registry/submodel-descriptors/{submodel_id}` | DELETE | Deletes a Submodel Descriptor | ✅ | ## Discovery Service | Endpoint | Operation | Description | Status | From 41aafb8656ce6d12ff92e4fd24e0f0ee07a7226b Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Tue, 14 Jan 2025 17:33:55 +0100 Subject: [PATCH 14/45] api/server: Fix imports to support unittests. --- api/server/__init__.py | 1 + api/server/routes/aas_registry_server.py | 2 +- api/server/routes/aasx_file_server.py | 2 +- api/server/routes/submodel.py | 2 +- api/server/routes/submodel_registry_server.py | 2 +- api/server/server.py | 2 +- api/test/test_server.py | 17 +++++++++++++++++ sdk/basyx/object_store.py | 6 +++--- 8 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 api/test/test_server.py diff --git a/api/server/__init__.py b/api/server/__init__.py index e69de29..ff892ce 100644 --- a/api/server/__init__.py +++ b/api/server/__init__.py @@ -0,0 +1 @@ +from .server import app diff --git a/api/server/routes/aas_registry_server.py b/api/server/routes/aas_registry_server.py index 647aef4..81d5db3 100644 --- a/api/server/routes/aas_registry_server.py +++ b/api/server/routes/aas_registry_server.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Request, HTTPException from basyx import ObjectStore -from services.aas_registry_server_service import AasRegistryServerService +from ..services.aas_registry_server_service import AasRegistryServerService class AasRegistryRouter: diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index 69e4c39..f0d8df1 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Request, HTTPException from basyx import ObjectStore -from services.aasx_flie_server_service import AasxFileServerService +from ..services.aasx_flie_server_service import AasxFileServerService class AasxFileServerRouter: diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index 3365508..c59f977 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Request, HTTPException from basyx import ObjectStore -from services.submodel_service import SubmodelService +from ..services.submodel_service import SubmodelService class SubmodelRouter: diff --git a/api/server/routes/submodel_registry_server.py b/api/server/routes/submodel_registry_server.py index c9b39b6..dd2640d 100644 --- a/api/server/routes/submodel_registry_server.py +++ b/api/server/routes/submodel_registry_server.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Request, HTTPException from basyx import ObjectStore -from services.submodel_registry_server_service import SubmodelRegistryServerService +from ..services.submodel_registry_server_service import SubmodelRegistryServerService class SubmodelRegistryRouter: diff --git a/api/server/server.py b/api/server/server.py index 0e15fb6..af33fcb 100644 --- a/api/server/server.py +++ b/api/server/server.py @@ -2,7 +2,7 @@ import uvicorn # Import routers -from routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server +from .routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server from basyx import object_store diff --git a/api/test/test_server.py b/api/test/test_server.py new file mode 100644 index 0000000..bbac675 --- /dev/null +++ b/api/test/test_server.py @@ -0,0 +1,17 @@ +import unittest +from fastapi.testclient import TestClient +from server import app # Import your FastAPI app + +# Create a TestClient instance for your app +client = TestClient(app) + +class TestFastAPIEndpoints(unittest.TestCase): + def test_registry_submodel_descriptors(self): + # Test the GET /items/{item_id} endpoint + response = client.get("/api/v3.0/submodels/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/sdk/basyx/object_store.py b/sdk/basyx/object_store.py index 090b486..851e5ec 100644 --- a/sdk/basyx/object_store.py +++ b/sdk/basyx/object_store.py @@ -190,16 +190,16 @@ def get_parent_referable(self, id_short: str) -> Referable: return element raise KeyError("there is no parent Identifiable for id_short {}".format(id_short)) - def filter_identifiables_by_instance(self, instance: Type) -> list[Type]: + def get_identifiables_by_type(self, t: Type) -> list[Type]: """ Get all identifiables of the specified type. - :param instance: The Type to filter by. For example, we can filter by "aas_core3.types.ConceptDescription" + :param t: The Type to filter by. For example, we can filter by "aas_core3.types.ConceptDescription" :return: The list of the filtered identifiables """ filtered_identifiables = [] for identifiable in self._backend.values(): - if isinstance(identifiable, instance): + if isinstance(identifiable, t): filtered_identifiables.append(identifiable) return filtered_identifiables From b48aa8ef1bd70f4216dc9dd1ac6643a9211a39b2 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Mon, 20 Jan 2025 14:57:15 +0100 Subject: [PATCH 15/45] api/server: Add submodelElement related endpoints Add some testcases covering basic submodel interaction Add GET/POST/PUT/DELETE endpoints regarding specific submodelElements in a submodel --- api/README.md | 10 +-- api/server/routes/submodel.py | 16 ++-- api/server/services/submodel_service.py | 66 ++++++++++++++- api/test/test_server.py | 102 +++++++++++++++++++++++- 4 files changed, 174 insertions(+), 20 deletions(-) diff --git a/api/README.md b/api/README.md index fb49169..9534f0c 100644 --- a/api/README.md +++ b/api/README.md @@ -46,15 +46,15 @@ Below is the status table for the endpoints, organized as specified. | `/submodels/{submodel_id}/$reference` | GET | Retrieve reference of a specific submodel | 📅 | | `/submodels/{submodel_id}/$path` | GET | Retrieve a specific submodel by path | ❌ | | `/submodels/{submodel_id}/submodel-elements` | GET | Retrieve all elements of a specific submodel | ✅ | -| `/submodels/{submodel_id}/submodel-elements` | POST | Create new elements in a specific submodel | 📅 | +| `/submodels/{submodel_id}/submodel-elements` | POST | Create new elements in a specific submodel | ✅ | | `/submodels/{submodel_id}/submodel-elements/$metadata` | GET | Retrieve metadata for submodel elements | 📅 | | `/submodels/{submodel_id}/submodel-elements/$reference` | GET | Retrieve references for submodel elements | 📅 | | `/submodels/{submodel_id}/submodel-elements/$value` | GET | Retrieve values for submodel elements | ❌ | | `/submodels/{submodel_id}/submodel-elements/$path` | GET | Retrieve elements by path in a specific submodel | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | GET | Retrieve specific elements by short ID in a submodel | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | POST | Create specific elements by short ID in a submodel | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PUT | Update specific elements by short ID in a submodel | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | Delete specific elements by short ID in a submodel | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | GET | Retrieve specific elements by short ID in a submodel | ✅ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | POST | Create specific elements by short ID in a submodel | ✅ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PUT | Update specific elements by short ID in a submodel | ✅ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | Delete specific elements by short ID in a submodel | ✅ | | `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PATCH | Partially update specific elements by short ID in a submodel | ❌ | | `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | GET | Retrieve metadata of specific elements by short ID | 📅 | | `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | PATCH | Update metadata of specific elements by short ID | ❌ | diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index c59f977..cd1b4f1 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -50,7 +50,7 @@ async def get_submodel(submodel_id: str) -> Any: async def put_submodel(submodel_id: str, request: Request) -> Any: # Update submodel with given id body = await request.json() - return self.service.update_submode_by_id(submodel_id, body) + return self.service.update_submodel_by_id(submodel_id, body) @self.router.delete("/{submodel_id}") async def delete_submodel(submodel_id: str) -> Any: @@ -112,19 +112,21 @@ async def not_implemented_submodel_elements_path(submodel_id: str) -> Any: @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}") async def get_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not yet implemented!") + return self.service.get_submodel_element(submodel_id, id_shorts) @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}") - async def post_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not yet implemented!") + async def post_submodel_submodel_elements_id_short_path(submodel_id: str, request: Request) -> Any: + body = await request.json() + return self.service.post_submodel_element(submodel_id, body) @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}") - async def put_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not yet implemented!") + async def put_submodel_submodel_elements_id_short_path(submodel_id: str, request: Request) -> Any: + body = await request.json() + return self.service.put_submodel_element(submodel_id, body) @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}") async def delete_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not yet implemented!") + return self.service.delete_submodel_element(submodel_id, id_shorts) @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}") async def not_implemented_patch_submodel_submodel_elements_id_short_path(submodel_id: str, diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index e1392f0..b3e89df 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -25,8 +25,16 @@ def _get_submodel_by_id(self, submodel_id): raise HTTPException(status_code=404, detail="Submodel with id " + submodel_id + " not found") return submodel + def _get_submodel_element_by_id_short(self, submodel_id, element_id_short): + submodel = self._get_submodel_by_id(submodel_id) + for element in submodel.descend(): + if isinstance(element, SubmodelElement): + if element.id_short == element_id_short: + return element + return None + # Endpoint specific logic - def get_all_submodels_as_jsonables(self)\ + def get_all_submodels_as_jsonables(self) \ -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: return self._jsonable_submodels(self._get_all_submodels()) @@ -44,12 +52,11 @@ def get_submodel_jsonable_by_id(self, submodel_id: str): submodel = self._get_submodel_by_id(submodel_id) return jsonization.to_jsonable(submodel) - def update_submode_by_id(self, submodel_id: str, json): + def update_submodel_by_id(self, submodel_id: str, json): submodel = self._get_submodel_by_id(submodel_id) new_submodel = jsonization.submodel_from_jsonable(json) if submodel.id != new_submodel.id: raise HTTPException(403, "Submodel with id " + submodel_id + " does not match") - # TODO: This cant be right self.obj_store.discard(submodel) self.obj_store.add(new_submodel) return jsonization.to_jsonable(new_submodel) @@ -60,4 +67,55 @@ def delete_submodel_by_id(self, submodel_id): return {"message": "Submodel with id " + submodel_id + " deleted successfully"} def get_submodel_elements(self, submodel_id): - pass + submodel = self._get_submodel_by_id(submodel_id) + elements = [] + for element in submodel.descend(): + # Maybe filter some items out? + elements.append(jsonization.to_jsonable(element)) + return elements + + def update_submodel_elements(self, submodel_id, json): + submodel = self._get_submodel_by_id(submodel_id) + new_elements = [] + for element in json: + deserialized_element = jsonization.submodel_element_from_jsonable(element) + new_elements.append(deserialized_element) + self.obj_store.discard(submodel) + submodel.submodel_elements = new_elements + self.obj_store.add(submodel) + + def get_submodel_element(self, submodel_id, element_short_id): + element = self._get_submodel_element_by_id_short(submodel_id, element_short_id) + if element is None: + raise HTTPException(status_code=404, detail="Submodel element with id " + element_short_id + " not found.") + return jsonization.to_jsonable(element) + + def post_submodel_element(self, submodel_id, body): + submodel = self._get_submodel_by_id(submodel_id) + submodel_element = jsonization.submodel_element_from_jsonable(body) + existing_submodel_element = self._get_submodel_element_by_id_short(submodel_id, submodel_element.id_short) + if existing_submodel_element is None: + submodel.submodel_elements.append(submodel_element) + return jsonization.to_jsonable(submodel_element) + else: + raise HTTPException(status_code=400, detail="Submodel element with id " + submodel_element.id_short + " already exists") + + def put_submodel_element(self, submodel_id, body): + submodel = self._get_submodel_by_id(submodel_id) + submodel_element = jsonization.submodel_element_from_jsonable(body) + existing_submodel_element = self._get_submodel_element_by_id_short(submodel_id, submodel_element.id_short) + if existing_submodel_element is None: + raise HTTPException(status_code=404, detail="Submodel element with id " + submodel_element.id_short + " does not exist") + else: + submodel.submodel_elements.remove(existing_submodel_element) + submodel.submodel_elements.append(submodel_element) + return jsonization.to_jsonable(submodel_element) + + def delete_submodel_element(self, submodel_id, id_short): + submodel = self._get_submodel_by_id(submodel_id) + existing_submodel_element = self._get_submodel_element_by_id_short(submodel_id, id_short) + if existing_submodel_element is None: + raise HTTPException(status_code=404, detail="Submodel element with id " + id_short + " does not exist") + else: + submodel.submodel_elements.remove(existing_submodel_element) + return jsonization.to_jsonable(existing_submodel_element) diff --git a/api/test/test_server.py b/api/test/test_server.py index bbac675..0ba207b 100644 --- a/api/test/test_server.py +++ b/api/test/test_server.py @@ -1,17 +1,111 @@ import unittest from fastapi.testclient import TestClient -from server import app # Import your FastAPI app +from server import app -# Create a TestClient instance for your app client = TestClient(app) +BASE_URL = "/api/v3.0/" class TestFastAPIEndpoints(unittest.TestCase): - def test_registry_submodel_descriptors(self): + + test_submodel = { + "id": "urn:x-test:submodel1", + "submodelElements": [ + { + "idShort": "some_property", + "valueType": "xs:int", + "value": "1984", + "modelType": "Property" + }, + { + "idShort": "some_blob", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "ExampleSubmodelList", + "typeValueListElement": "SubmodelElementList", + "value": [ + { + "idShort": "list_1", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "list_2", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + } + ], + "modelType": "SubmodelElementList" + } + ], + "modelType": "Submodel" + } + test_submodel_modified = { + "id": "urn:x-test:submodel1", + "submodelElements": [ + { + "idShort": "some_property", + "valueType": "xs:int", + "value": "8419", + "modelType": "Property" + }, + { + "idShort": "some_blob", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "ExampleSubmodelList", + "typeValueListElement": "SubmodelElementList", + "value": [ + { + "idShort": "list_1", + "value": "481563", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "list_2", + "value": "&/6453=(", + "contentType": "application/octet-stream", + "modelType": "Blob" + } + ], + "modelType": "SubmodelElementList" + } + ], + "modelType": "Submodel" + } + test_submode_id = test_submodel["id"] + invalid_submode_id = "some_blob" + + def test_01_(self): # Test the GET /items/{item_id} endpoint - response = client.get("/api/v3.0/submodels/") + response = client.get(BASE_URL + "submodels/") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), []) + def test_02_post_submodel(self): + response = client.post(BASE_URL + "submodels/", json=self.test_submodel) + self.assertEqual(response.status_code, 200) + + def test_03_get_submodel_undefined(self): + response_test_undefined = client.get(BASE_URL + "submodels/" + self.invalid_submode_id + "/") + self.assertEqual(response_test_undefined.status_code, 404) + + def test_04_get_submodel(self): + response_test_entry = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/") + self.assertEqual(response_test_entry.status_code, 200) + self.assertEqual(response_test_entry.json(), self.test_submodel) + + def test_05_get_submodel_element(self): + response = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/submodel-elements/" + "list_1") + self.assertEqual(response.status_code, 200) if __name__ == "__main__": unittest.main() From 7c4adeb9cfc7b9b00d0f297503a7c2394d095699 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Tue, 4 Feb 2025 15:21:28 +0100 Subject: [PATCH 16/45] Restructure imports following PEP 8 --- api/server/routes/aas.py | 5 +++-- api/server/routes/aas_registry_server.py | 6 +++--- api/server/routes/aasx_file_server.py | 6 +++--- api/server/routes/submodel.py | 4 ++-- api/server/routes/submodel_registry_server.py | 6 +++--- api/server/server.py | 8 +++----- api/server/services/aas_registry_server_service.py | 11 ++++++----- api/server/services/{aas_service.py => aasservice.py} | 2 +- api/server/services/aasx_flie_server_service.py | 6 ++++-- .../services/submodel_registry_server_service.py | 11 ++++++----- api/server/services/submodel_service.py | 4 ++-- api/test/test_server.py | 3 ++- 12 files changed, 38 insertions(+), 34 deletions(-) rename api/server/services/{aas_service.py => aasservice.py} (87%) diff --git a/api/server/routes/aas.py b/api/server/routes/aas.py index 3c81e1c..3d62a55 100644 --- a/api/server/routes/aas.py +++ b/api/server/routes/aas.py @@ -1,9 +1,10 @@ -from fastapi import APIRouter from typing import Any -router = APIRouter() +from fastapi import APIRouter +router = APIRouter() + @router.get("/aas") async def get_all_aas() -> Any: return {"message": ""} diff --git a/api/server/routes/aas_registry_server.py b/api/server/routes/aas_registry_server.py index 81d5db3..b659ee3 100644 --- a/api/server/routes/aas_registry_server.py +++ b/api/server/routes/aas_registry_server.py @@ -1,10 +1,10 @@ from typing import Any from aas_core3.types import Identifiable -from fastapi import APIRouter, Request, HTTPException +from fastapi import APIRouter, Request -from basyx import ObjectStore -from ..services.aas_registry_server_service import AasRegistryServerService +from api.server.services.aas_registry_server_service import AasRegistryServerService +from sdk.basyx import ObjectStore class AasRegistryRouter: diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index f0d8df1..4a1f625 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -1,10 +1,10 @@ from typing import Any from aas_core3.types import Identifiable -from fastapi import APIRouter, Request, HTTPException +from fastapi import APIRouter, Request -from basyx import ObjectStore -from ..services.aasx_flie_server_service import AasxFileServerService +from api.server.services.aasx_flie_server_service import AasxFileServerService +from sdk.basyx import ObjectStore class AasxFileServerRouter: diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index cd1b4f1..741d4f7 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -3,8 +3,8 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request, HTTPException -from basyx import ObjectStore -from ..services.submodel_service import SubmodelService +from api.server.services.submodel_service import SubmodelService +from sdk.basyx import ObjectStore class SubmodelRouter: diff --git a/api/server/routes/submodel_registry_server.py b/api/server/routes/submodel_registry_server.py index dd2640d..c6af999 100644 --- a/api/server/routes/submodel_registry_server.py +++ b/api/server/routes/submodel_registry_server.py @@ -1,10 +1,10 @@ from typing import Any from aas_core3.types import Identifiable -from fastapi import APIRouter, Request, HTTPException +from fastapi import APIRouter, Request -from basyx import ObjectStore -from ..services.submodel_registry_server_service import SubmodelRegistryServerService +from api.server.services.submodel_registry_server_service import SubmodelRegistryServerService +from sdk.basyx import ObjectStore class SubmodelRegistryRouter: diff --git a/api/server/server.py b/api/server/server.py index af33fcb..193733c 100644 --- a/api/server/server.py +++ b/api/server/server.py @@ -1,10 +1,8 @@ -from fastapi import FastAPI import uvicorn +from fastapi import FastAPI -# Import routers -from .routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server - -from basyx import object_store +from api.server.routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server +from sdk.basyx import object_store app = FastAPI() prefix = "/api/v3.0" diff --git a/api/server/services/aas_registry_server_service.py b/api/server/services/aas_registry_server_service.py index 0bb2028..028365c 100644 --- a/api/server/services/aas_registry_server_service.py +++ b/api/server/services/aas_registry_server_service.py @@ -1,9 +1,10 @@ -from basyx import ObjectStore -from aas_core3.types import AssetAdministrationShell, ConceptDescription -from aas_core3 import jsonization -from fastapi import HTTPException from typing import Any, MutableMapping + from aas_core3 import jsonization +from aas_core3.types import AssetAdministrationShell, ConceptDescription +from fastapi import HTTPException + +from sdk.basyx import ObjectStore class AasRegistryServerService: @@ -11,7 +12,7 @@ def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store def GetAllAssetAdministrationShellDescriptors(self) -> list[str]: - all_descriptors = self.obj_store.filter_identifiables_by_instance(ConceptDescription) + all_descriptors = self.obj_store.get_identifiables_by_type(ConceptDescription) print(all_descriptors.__dict__) print(all_descriptors) print("test") diff --git a/api/server/services/aas_service.py b/api/server/services/aasservice.py similarity index 87% rename from api/server/services/aas_service.py rename to api/server/services/aasservice.py index d3ffccf..240f800 100644 --- a/api/server/services/aas_service.py +++ b/api/server/services/aasservice.py @@ -1,6 +1,6 @@ from sdk.basyx import ObjectStore -class aas_service: +class AasService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store diff --git a/api/server/services/aasx_flie_server_service.py b/api/server/services/aasx_flie_server_service.py index b1abd9c..a9c2d43 100644 --- a/api/server/services/aasx_flie_server_service.py +++ b/api/server/services/aasx_flie_server_service.py @@ -1,8 +1,10 @@ -from basyx import ObjectStore +from typing import Any, MutableMapping + from aas_core3.types import AssetAdministrationShell from aas_core3 import jsonization from fastapi import HTTPException -from typing import Any, MutableMapping + +from sdk.basyx import ObjectStore class AasxFileServerService: diff --git a/api/server/services/submodel_registry_server_service.py b/api/server/services/submodel_registry_server_service.py index e43c3a0..807f617 100644 --- a/api/server/services/submodel_registry_server_service.py +++ b/api/server/services/submodel_registry_server_service.py @@ -1,9 +1,10 @@ -from basyx import ObjectStore -from aas_core3.types import AssetAdministrationShell, Submodel +from typing import Any, MutableMapping + from aas_core3 import jsonization +from aas_core3.types import Submodel, ConceptDescription from fastapi import HTTPException -from typing import Any, MutableMapping -from aas_core3.types import AssetAdministrationShell, ConceptDescription + +from sdk.basyx import ObjectStore class SubmodelRegistryServerService: @@ -13,7 +14,7 @@ def __init__(self, global_object_store: ObjectStore): def GetAllSubmodelDescriptors(self) -> list[str]: #print(self.obj_store.__dict__) - all_descriptors = self.obj_store.filter_identifiables_by_instance(ConceptDescription) + all_descriptors = self.obj_store.get_identifiables_by_type(ConceptDescription) #print(all_descriptors.__dict__) print(all_descriptors) #print("test") diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index b3e89df..bedb917 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -1,10 +1,10 @@ from typing import Any, MutableMapping from aas_core3 import jsonization -from aas_core3.types import Submodel +from aas_core3.types import Submodel, SubmodelElement from fastapi import HTTPException -from basyx import ObjectStore +from sdk.basyx import ObjectStore class SubmodelService: diff --git a/api/test/test_server.py b/api/test/test_server.py index 0ba207b..1d3a3e4 100644 --- a/api/test/test_server.py +++ b/api/test/test_server.py @@ -1,6 +1,7 @@ import unittest from fastapi.testclient import TestClient -from server import app + +from api.server import app client = TestClient(app) BASE_URL = "/api/v3.0/" From 55bc7d210d56dfd72b96160f13fe983ff72874d7 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Fri, 7 Feb 2025 19:51:40 +0100 Subject: [PATCH 17/45] (Hopefully) Resolve imports conflicts by using primarily absolute imports --- api/server/__init__.py | 2 +- api/server/routes/aas_registry_server.py | 4 ++-- api/server/routes/aasx_file_server.py | 4 ++-- api/server/routes/submodel.py | 4 ++-- api/server/routes/submodel_registry_server.py | 4 ++-- api/server/{server.py => server_main.py} | 4 ++-- api/server/services/aas_registry_server_service.py | 2 +- api/server/services/aasservice.py | 2 +- api/server/services/aasx_flie_server_service.py | 2 +- api/server/services/submodel_registry_server_service.py | 2 +- api/server/services/submodel_service.py | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) rename api/server/{server.py => server_main.py} (87%) diff --git a/api/server/__init__.py b/api/server/__init__.py index ff892ce..2e14053 100644 --- a/api/server/__init__.py +++ b/api/server/__init__.py @@ -1 +1 @@ -from .server import app +from .server_main import app diff --git a/api/server/routes/aas_registry_server.py b/api/server/routes/aas_registry_server.py index b659ee3..d19a260 100644 --- a/api/server/routes/aas_registry_server.py +++ b/api/server/routes/aas_registry_server.py @@ -3,8 +3,8 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request -from api.server.services.aas_registry_server_service import AasRegistryServerService -from sdk.basyx import ObjectStore +from server.services.aas_registry_server_service import AasRegistryServerService +from basyx import ObjectStore class AasRegistryRouter: diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index 4a1f625..777b9f3 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -3,8 +3,8 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request -from api.server.services.aasx_flie_server_service import AasxFileServerService -from sdk.basyx import ObjectStore +from server.services.aasx_flie_server_service import AasxFileServerService +from basyx import ObjectStore class AasxFileServerRouter: diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index 741d4f7..f9644f9 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -3,8 +3,8 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request, HTTPException -from api.server.services.submodel_service import SubmodelService -from sdk.basyx import ObjectStore +from server.services.submodel_service import SubmodelService +from basyx import ObjectStore class SubmodelRouter: diff --git a/api/server/routes/submodel_registry_server.py b/api/server/routes/submodel_registry_server.py index c6af999..d3a29f0 100644 --- a/api/server/routes/submodel_registry_server.py +++ b/api/server/routes/submodel_registry_server.py @@ -3,8 +3,8 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request -from api.server.services.submodel_registry_server_service import SubmodelRegistryServerService -from sdk.basyx import ObjectStore +from server.services.submodel_registry_server_service import SubmodelRegistryServerService +from basyx import ObjectStore class SubmodelRegistryRouter: diff --git a/api/server/server.py b/api/server/server_main.py similarity index 87% rename from api/server/server.py rename to api/server/server_main.py index 193733c..0e4fd6a 100644 --- a/api/server/server.py +++ b/api/server/server_main.py @@ -1,8 +1,8 @@ import uvicorn from fastapi import FastAPI -from api.server.routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server -from sdk.basyx import object_store +from server.routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server +from basyx import object_store app = FastAPI() prefix = "/api/v3.0" diff --git a/api/server/services/aas_registry_server_service.py b/api/server/services/aas_registry_server_service.py index 028365c..c1eb4ed 100644 --- a/api/server/services/aas_registry_server_service.py +++ b/api/server/services/aas_registry_server_service.py @@ -4,7 +4,7 @@ from aas_core3.types import AssetAdministrationShell, ConceptDescription from fastapi import HTTPException -from sdk.basyx import ObjectStore +from basyx import ObjectStore class AasRegistryServerService: diff --git a/api/server/services/aasservice.py b/api/server/services/aasservice.py index 240f800..88dc49a 100644 --- a/api/server/services/aasservice.py +++ b/api/server/services/aasservice.py @@ -1,4 +1,4 @@ -from sdk.basyx import ObjectStore +from basyx import ObjectStore class AasService: diff --git a/api/server/services/aasx_flie_server_service.py b/api/server/services/aasx_flie_server_service.py index a9c2d43..623a861 100644 --- a/api/server/services/aasx_flie_server_service.py +++ b/api/server/services/aasx_flie_server_service.py @@ -4,7 +4,7 @@ from aas_core3 import jsonization from fastapi import HTTPException -from sdk.basyx import ObjectStore +from basyx import ObjectStore class AasxFileServerService: diff --git a/api/server/services/submodel_registry_server_service.py b/api/server/services/submodel_registry_server_service.py index 807f617..040aa8f 100644 --- a/api/server/services/submodel_registry_server_service.py +++ b/api/server/services/submodel_registry_server_service.py @@ -4,7 +4,7 @@ from aas_core3.types import Submodel, ConceptDescription from fastapi import HTTPException -from sdk.basyx import ObjectStore +from basyx import ObjectStore class SubmodelRegistryServerService: diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index bedb917..6d49c8e 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -4,7 +4,7 @@ from aas_core3.types import Submodel, SubmodelElement from fastapi import HTTPException -from sdk.basyx import ObjectStore +from basyx import ObjectStore class SubmodelService: From 8120a88a5907a5d063ebfc71a8559b0cc983b688 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 13:18:29 +0100 Subject: [PATCH 18/45] modify testing structure in /api/server/test --- api/test/__init__.py | 0 api/test/examples/__init__.py | 0 api/test/examples/aasx_packages.py | 5 + api/test/examples/submodels.py | 74 ++++++++++++++ api/test/test_aasx_file_server_service.py | 42 ++++++++ api/test/test_server.py | 112 ---------------------- api/test/test_submodel_service.py | 40 ++++++++ 7 files changed, 161 insertions(+), 112 deletions(-) create mode 100644 api/test/__init__.py create mode 100644 api/test/examples/__init__.py create mode 100644 api/test/examples/aasx_packages.py create mode 100644 api/test/examples/submodels.py create mode 100644 api/test/test_aasx_file_server_service.py delete mode 100644 api/test/test_server.py create mode 100644 api/test/test_submodel_service.py diff --git a/api/test/__init__.py b/api/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/test/examples/__init__.py b/api/test/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/test/examples/aasx_packages.py b/api/test/examples/aasx_packages.py new file mode 100644 index 0000000..cb10707 --- /dev/null +++ b/api/test/examples/aasx_packages.py @@ -0,0 +1,5 @@ +from aas_core3.types import AssetAdministrationShell +from aas_core3 import jsonization + +json = {"id": "aasx_package_test", "assetInformation": {"assetKind": "Type"}, "modelType": "AssetAdministrationShell"} +aasx_package = jsonization.asset_administration_shell_from_jsonable(json) diff --git a/api/test/examples/submodels.py b/api/test/examples/submodels.py new file mode 100644 index 0000000..951880c --- /dev/null +++ b/api/test/examples/submodels.py @@ -0,0 +1,74 @@ +test_submodel = { + "id": "urn:x-test:submodel1", + "submodelElements": [ + { + "idShort": "some_property", + "valueType": "xs:int", + "value": "1984", + "modelType": "Property" + }, + { + "idShort": "some_blob", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "ExampleSubmodelList", + "typeValueListElement": "SubmodelElementList", + "value": [ + { + "idShort": "list_1", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "list_2", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + } + ], + "modelType": "SubmodelElementList" + } + ], + "modelType": "Submodel" + } +test_submodel_modified = { + "id": "urn:x-test:submodel1", + "submodelElements": [ + { + "idShort": "some_property", + "valueType": "xs:int", + "value": "8419", + "modelType": "Property" + }, + { + "idShort": "some_blob", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "ExampleSubmodelList", + "typeValueListElement": "SubmodelElementList", + "value": [ + { + "idShort": "list_1", + "value": "481563", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "list_2", + "value": "&/6453=(", + "contentType": "application/octet-stream", + "modelType": "Blob" + } + ], + "modelType": "SubmodelElementList" + } + ], + "modelType": "Submodel" +} \ No newline at end of file diff --git a/api/test/test_aasx_file_server_service.py b/api/test/test_aasx_file_server_service.py new file mode 100644 index 0000000..bb6d300 --- /dev/null +++ b/api/test/test_aasx_file_server_service.py @@ -0,0 +1,42 @@ +import unittest +from fastapi.testclient import TestClient + +from api.server import app +from .examples.aasx_packages import aasx_package +from .examples.submodels import test_submodel_modified, test_submodel + +client = TestClient(app) +BASE_URL = "/api/v3.0/" + + +class TestFastAPIEndpoints(unittest.TestCase): + test_submode_id = test_submodel["id"] + invalid_submodel_id = "some_blob" + + def test_01_(self): + # Test the GET /items/{item_id} endpoint + response = client.get(BASE_URL + "aasx/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + def test_02_post_aasx_package(self): + + response = client.post(BASE_URL + "aasx/", json=test_submodel) + self.assertEqual(response.status_code, 200) + + def test_03_get_submodel_undefined(self): + response_test_undefined = client.get(BASE_URL + "aasx/" + self.invalid_submodel_id + "/") + self.assertEqual(response_test_undefined.status_code, 404) + + def test_04_get_submodel(self): + response_test_entry = client.get(BASE_URL + "aasx/" + self.test_submode_id + "/") + self.assertEqual(response_test_entry.status_code, 200) + self.assertEqual(response_test_entry.json(), test_submodel) + + def test_05_get_submodel_element(self): + response = client.get(BASE_URL + "aasx/" + self.test_submode_id + "/submodel-elements/" + "list_1") + self.assertEqual(response.status_code, 200) + + +if __name__ == "__main__": + unittest.main() diff --git a/api/test/test_server.py b/api/test/test_server.py deleted file mode 100644 index 1d3a3e4..0000000 --- a/api/test/test_server.py +++ /dev/null @@ -1,112 +0,0 @@ -import unittest -from fastapi.testclient import TestClient - -from api.server import app - -client = TestClient(app) -BASE_URL = "/api/v3.0/" - -class TestFastAPIEndpoints(unittest.TestCase): - - test_submodel = { - "id": "urn:x-test:submodel1", - "submodelElements": [ - { - "idShort": "some_property", - "valueType": "xs:int", - "value": "1984", - "modelType": "Property" - }, - { - "idShort": "some_blob", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "ExampleSubmodelList", - "typeValueListElement": "SubmodelElementList", - "value": [ - { - "idShort": "list_1", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "list_2", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - } - ], - "modelType": "SubmodelElementList" - } - ], - "modelType": "Submodel" - } - test_submodel_modified = { - "id": "urn:x-test:submodel1", - "submodelElements": [ - { - "idShort": "some_property", - "valueType": "xs:int", - "value": "8419", - "modelType": "Property" - }, - { - "idShort": "some_blob", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "ExampleSubmodelList", - "typeValueListElement": "SubmodelElementList", - "value": [ - { - "idShort": "list_1", - "value": "481563", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "list_2", - "value": "&/6453=(", - "contentType": "application/octet-stream", - "modelType": "Blob" - } - ], - "modelType": "SubmodelElementList" - } - ], - "modelType": "Submodel" - } - test_submode_id = test_submodel["id"] - invalid_submode_id = "some_blob" - - def test_01_(self): - # Test the GET /items/{item_id} endpoint - response = client.get(BASE_URL + "submodels/") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_02_post_submodel(self): - response = client.post(BASE_URL + "submodels/", json=self.test_submodel) - self.assertEqual(response.status_code, 200) - - def test_03_get_submodel_undefined(self): - response_test_undefined = client.get(BASE_URL + "submodels/" + self.invalid_submode_id + "/") - self.assertEqual(response_test_undefined.status_code, 404) - - def test_04_get_submodel(self): - response_test_entry = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/") - self.assertEqual(response_test_entry.status_code, 200) - self.assertEqual(response_test_entry.json(), self.test_submodel) - - def test_05_get_submodel_element(self): - response = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/submodel-elements/" + "list_1") - self.assertEqual(response.status_code, 200) - -if __name__ == "__main__": - unittest.main() diff --git a/api/test/test_submodel_service.py b/api/test/test_submodel_service.py new file mode 100644 index 0000000..01b2884 --- /dev/null +++ b/api/test/test_submodel_service.py @@ -0,0 +1,40 @@ +import unittest +from fastapi.testclient import TestClient + +from api.server import app +from .examples.submodels import test_submodel_modified, test_submodel + +client = TestClient(app) +BASE_URL = "/api/v3.0/" + + +class TestFastAPIEndpoints(unittest.TestCase): + test_submode_id = test_submodel["id"] + invalid_submode_id = "some_blob" + + def test_01_(self): + # Test the GET /items/{item_id} endpoint + response = client.get(BASE_URL + "submodels/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + def test_02_post_submodel(self): + response = client.post(BASE_URL + "submodels/", json=test_submodel) + self.assertEqual(response.status_code, 200) + + def test_03_get_submodel_undefined(self): + response_test_undefined = client.get(BASE_URL + "submodels/" + self.invalid_submode_id + "/") + self.assertEqual(response_test_undefined.status_code, 404) + + def test_04_get_submodel(self): + response_test_entry = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/") + self.assertEqual(response_test_entry.status_code, 200) + self.assertEqual(response_test_entry.json(), test_submodel) + + def test_05_get_submodel_element(self): + response = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/submodel-elements/" + "list_1") + self.assertEqual(response.status_code, 200) + + +if __name__ == "__main__": + unittest.main() From ea18cc7550a6be3f67912adefe9f47d3601e8047 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 13:50:24 +0100 Subject: [PATCH 19/45] fix issue with compatibility to python 3.8 --- sdk/basyx/object_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/basyx/object_store.py b/sdk/basyx/object_store.py index 851e5ec..b94c03d 100644 --- a/sdk/basyx/object_store.py +++ b/sdk/basyx/object_store.py @@ -190,7 +190,7 @@ def get_parent_referable(self, id_short: str) -> Referable: return element raise KeyError("there is no parent Identifiable for id_short {}".format(id_short)) - def get_identifiables_by_type(self, t: Type) -> list[Type]: + def get_identifiables_by_type(self, t: Type) -> List[Type]: """ Get all identifiables of the specified type. From b5846db3680c4319927d83988826866e5dc56001 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 13:56:19 +0100 Subject: [PATCH 20/45] Add testfiles and include server tests in CI --- api/test/examples/aasx_packages.py | 5 ++-- api/test/test_aasx_file_server_service.py | 29 ++++++++++------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/api/test/examples/aasx_packages.py b/api/test/examples/aasx_packages.py index cb10707..bf12187 100644 --- a/api/test/examples/aasx_packages.py +++ b/api/test/examples/aasx_packages.py @@ -1,5 +1,4 @@ -from aas_core3.types import AssetAdministrationShell from aas_core3 import jsonization -json = {"id": "aasx_package_test", "assetInformation": {"assetKind": "Type"}, "modelType": "AssetAdministrationShell"} -aasx_package = jsonization.asset_administration_shell_from_jsonable(json) +aasx_package_json = {"id": "aasx_package_test", "assetInformation": {"assetKind": "Type"}, "modelType": "AssetAdministrationShell"} +aasx_package = jsonization.asset_administration_shell_from_jsonable(aasx_package_json) diff --git a/api/test/test_aasx_file_server_service.py b/api/test/test_aasx_file_server_service.py index bb6d300..06e3803 100644 --- a/api/test/test_aasx_file_server_service.py +++ b/api/test/test_aasx_file_server_service.py @@ -2,7 +2,7 @@ from fastapi.testclient import TestClient from api.server import app -from .examples.aasx_packages import aasx_package +from .examples.aasx_packages import aasx_package_json from .examples.submodels import test_submodel_modified, test_submodel client = TestClient(app) @@ -10,32 +10,29 @@ class TestFastAPIEndpoints(unittest.TestCase): - test_submode_id = test_submodel["id"] + test_aasx_package_id = aasx_package_json["id"] invalid_submodel_id = "some_blob" - def test_01_(self): - # Test the GET /items/{item_id} endpoint + def test_01_get_all_aasx_package_ids(self): + # Test the GET / endpoint response = client.get(BASE_URL + "aasx/") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), []) def test_02_post_aasx_package(self): - - response = client.post(BASE_URL + "aasx/", json=test_submodel) + response = client.post(BASE_URL + "aasx/", json=aasx_package_json) self.assertEqual(response.status_code, 200) - def test_03_get_submodel_undefined(self): - response_test_undefined = client.get(BASE_URL + "aasx/" + self.invalid_submodel_id + "/") - self.assertEqual(response_test_undefined.status_code, 404) - - def test_04_get_submodel(self): - response_test_entry = client.get(BASE_URL + "aasx/" + self.test_submode_id + "/") + def test_03_get_aasx_package(self): + response_test_entry = client.get(BASE_URL + "aasx/" + self.test_aasx_package_id + "/") self.assertEqual(response_test_entry.status_code, 200) - self.assertEqual(response_test_entry.json(), test_submodel) + self.assertEqual(response_test_entry.json(), aasx_package_json) - def test_05_get_submodel_element(self): - response = client.get(BASE_URL + "aasx/" + self.test_submode_id + "/submodel-elements/" + "list_1") - self.assertEqual(response.status_code, 200) + def test_04_delete_aasx_package(self): + pass + + def test_05_put_aasx_package(self): + pass if __name__ == "__main__": From f33e70033dea5985b872380a7bc63490a37b5484 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 13:58:20 +0100 Subject: [PATCH 21/45] add server test to CI --- .github/workflows/ci.yml | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49d4c20..11a6c12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ env: X_PYTHON_VERSION: "3.10" jobs: - test: + sdk-test: runs-on: ubuntu-latest strategy: matrix: @@ -26,7 +26,7 @@ jobs: pip install coverage cd ./sdk pip install . - - name: Test with coverage + unittest + - name: Test sdk with coverage + unittest run: | cd ./sdk coverage run -m unittest @@ -36,6 +36,39 @@ jobs: cd ./sdk coverage report -m + api-test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.12"] + + + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install coverage + cd ./sdk + pip install . + cd .. + cd ./api + pip install . + - name: Test sdk with coverage + unittest + run: | + cd ./api + coverage run -m unittest + - name: Report test coverage + if: ${{ always() }} + run: | + cd ./api + coverage report -m + static-analysis: runs-on: ubuntu-latest From 25b7ffc09c57fb78dd98306b9d8a927b91bc2b6b Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 14:06:05 +0100 Subject: [PATCH 22/45] fix testing environment in CI --- .github/workflows/ci.yml | 2 +- api/pyproject.toml | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11a6c12..4d4fbd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: pip install . cd .. cd ./api - pip install . + pip install .[dev] - name: Test sdk with coverage + unittest run: | cd ./api diff --git a/api/pyproject.toml b/api/pyproject.toml index 00987ce..f5f9b9c 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -11,6 +11,16 @@ authors = [ {name = "The Eclipse BaSyx Authors"} ] # TODO: description is tbd -description="The Eclipse BaSyx Server does stuff" +description="The Eclipse BaSyx Server does stuff"[] readme = "README.md" license = {file = "./LICENSE"} + + +[project.optional-dependencies] +dev = [ + "mypy", + "pycodestyle", + "codeblocks", + "coverage", + "httpx" +] \ No newline at end of file From ed2e483b3dd39a525f5c984095db19ffab095889 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 14:07:19 +0100 Subject: [PATCH 23/45] fix typo --- api/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index f5f9b9c..987d077 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -11,7 +11,7 @@ authors = [ {name = "The Eclipse BaSyx Authors"} ] # TODO: description is tbd -description="The Eclipse BaSyx Server does stuff"[] +description="The Eclipse BaSyx Server does stuff" readme = "README.md" license = {file = "./LICENSE"} From c38c86da4cd8fe5b78352a7a98c9814739658363 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 14:12:28 +0100 Subject: [PATCH 24/45] fix yet another import mistake --- api/test/test_aasx_file_server_service.py | 2 +- api/test/test_submodel_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/test/test_aasx_file_server_service.py b/api/test/test_aasx_file_server_service.py index 06e3803..b59f9a1 100644 --- a/api/test/test_aasx_file_server_service.py +++ b/api/test/test_aasx_file_server_service.py @@ -1,7 +1,7 @@ import unittest from fastapi.testclient import TestClient -from api.server import app +from server import app from .examples.aasx_packages import aasx_package_json from .examples.submodels import test_submodel_modified, test_submodel diff --git a/api/test/test_submodel_service.py b/api/test/test_submodel_service.py index 01b2884..a6cbca1 100644 --- a/api/test/test_submodel_service.py +++ b/api/test/test_submodel_service.py @@ -1,7 +1,7 @@ import unittest from fastapi.testclient import TestClient -from api.server import app +from server import app from .examples.submodels import test_submodel_modified, test_submodel client = TestClient(app) From 582f44c22fad57459a884e2fdb862b60056ee225 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 14:16:19 +0100 Subject: [PATCH 25/45] fix multiple backwards compatibility problems --- api/server/services/aas_registry_server_service.py | 4 ++-- api/server/services/aasx_flie_server_service.py | 4 ++-- api/server/services/submodel_registry_server_service.py | 4 ++-- api/server/services/submodel_service.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/server/services/aas_registry_server_service.py b/api/server/services/aas_registry_server_service.py index c1eb4ed..b0b5489 100644 --- a/api/server/services/aas_registry_server_service.py +++ b/api/server/services/aas_registry_server_service.py @@ -1,4 +1,4 @@ -from typing import Any, MutableMapping +from typing import Any, MutableMapping, List from aas_core3 import jsonization from aas_core3.types import AssetAdministrationShell, ConceptDescription @@ -11,7 +11,7 @@ class AasRegistryServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def GetAllAssetAdministrationShellDescriptors(self) -> list[str]: + def GetAllAssetAdministrationShellDescriptors(self) -> List[str]: all_descriptors = self.obj_store.get_identifiables_by_type(ConceptDescription) print(all_descriptors.__dict__) print(all_descriptors) diff --git a/api/server/services/aasx_flie_server_service.py b/api/server/services/aasx_flie_server_service.py index 623a861..70d0f99 100644 --- a/api/server/services/aasx_flie_server_service.py +++ b/api/server/services/aasx_flie_server_service.py @@ -1,4 +1,4 @@ -from typing import Any, MutableMapping +from typing import Any, MutableMapping, List from aas_core3.types import AssetAdministrationShell from aas_core3 import jsonization @@ -11,7 +11,7 @@ class AasxFileServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def GetAllAASXPackageIds(self) -> list[str]: + def GetAllAASXPackageIds(self) -> List[str]: return [item.id for item in self.obj_store if isinstance(item, AssetAdministrationShell)] def GetAASXByPackageId(self, package_id) \ diff --git a/api/server/services/submodel_registry_server_service.py b/api/server/services/submodel_registry_server_service.py index 040aa8f..08310f5 100644 --- a/api/server/services/submodel_registry_server_service.py +++ b/api/server/services/submodel_registry_server_service.py @@ -1,4 +1,4 @@ -from typing import Any, MutableMapping +from typing import Any, MutableMapping, List from aas_core3 import jsonization from aas_core3.types import Submodel, ConceptDescription @@ -11,7 +11,7 @@ class SubmodelRegistryServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def GetAllSubmodelDescriptors(self) -> list[str]: + def GetAllSubmodelDescriptors(self) -> List[str]: #print(self.obj_store.__dict__) all_descriptors = self.obj_store.get_identifiables_by_type(ConceptDescription) diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index 6d49c8e..d150508 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -1,4 +1,4 @@ -from typing import Any, MutableMapping +from typing import Any, MutableMapping, List from aas_core3 import jsonization from aas_core3.types import Submodel, SubmodelElement @@ -12,11 +12,11 @@ def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store # General helper functions - def _get_all_submodels(self) -> list[Submodel]: + def _get_all_submodels(self) -> List[Submodel]: return [item for item in self.obj_store if isinstance(item, Submodel)] - def _jsonable_submodels(self, submodels: list[Submodel]) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + def _jsonable_submodels(self, submodels: List[Submodel]) \ + -> List[bool | int | float | str | List[Any] | MutableMapping[str, Any]]: return [jsonization.to_jsonable(submodel) for submodel in submodels] def _get_submodel_by_id(self, submodel_id): From 813e9615872803b5d93bb383fcf262d0ae7a767d Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 14:20:13 +0100 Subject: [PATCH 26/45] fix backwards compatility for python 3.8 --- api/server/services/submodel_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index d150508..710328a 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -1,4 +1,4 @@ -from typing import Any, MutableMapping, List +from typing import Any, MutableMapping, List, Union from aas_core3 import jsonization from aas_core3.types import Submodel, SubmodelElement @@ -16,7 +16,7 @@ def _get_all_submodels(self) -> List[Submodel]: return [item for item in self.obj_store if isinstance(item, Submodel)] def _jsonable_submodels(self, submodels: List[Submodel]) \ - -> List[bool | int | float | str | List[Any] | MutableMapping[str, Any]]: + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: return [jsonization.to_jsonable(submodel) for submodel in submodels] def _get_submodel_by_id(self, submodel_id): From b26f1b45d9f5bc051d44a5d463f49820423bb6c2 Mon Sep 17 00:00:00 2001 From: "simon.suchan" Date: Mon, 10 Feb 2025 14:23:24 +0100 Subject: [PATCH 27/45] fix more possible compatibility conflicts --- api/server/services/aas_registry_server_service.py | 4 ++-- api/server/services/aasx_flie_server_service.py | 4 ++-- api/server/services/submodel_registry_server_service.py | 4 ++-- api/server/services/submodel_service.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/server/services/aas_registry_server_service.py b/api/server/services/aas_registry_server_service.py index b0b5489..23a3382 100644 --- a/api/server/services/aas_registry_server_service.py +++ b/api/server/services/aas_registry_server_service.py @@ -1,4 +1,4 @@ -from typing import Any, MutableMapping, List +from typing import Any, MutableMapping, List, Union from aas_core3 import jsonization from aas_core3.types import AssetAdministrationShell, ConceptDescription @@ -38,7 +38,7 @@ def GetAllAssetAdministrationShellDescriptors(self) -> List[str]: return [jsonization.to_jsonable(descriptor) for descriptor in aas_descriptors_store] def GetAssetAdministrationShellDescriptorById(self, descriptor_id) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: aas_descriptor = self.obj_store.get_identifiable(descriptor_id) assert isinstance(aas_descriptor, ConceptDescription) return jsonization.to_jsonable(aas_descriptor) diff --git a/api/server/services/aasx_flie_server_service.py b/api/server/services/aasx_flie_server_service.py index 70d0f99..c256116 100644 --- a/api/server/services/aasx_flie_server_service.py +++ b/api/server/services/aasx_flie_server_service.py @@ -1,4 +1,4 @@ -from typing import Any, MutableMapping, List +from typing import Any, MutableMapping, List, Union from aas_core3.types import AssetAdministrationShell from aas_core3 import jsonization @@ -15,7 +15,7 @@ def GetAllAASXPackageIds(self) -> List[str]: return [item.id for item in self.obj_store if isinstance(item, AssetAdministrationShell)] def GetAASXByPackageId(self, package_id) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: aasx_package = self.obj_store.get_identifiable(package_id) assert isinstance(aasx_package, AssetAdministrationShell) return jsonization.to_jsonable(aasx_package) diff --git a/api/server/services/submodel_registry_server_service.py b/api/server/services/submodel_registry_server_service.py index 08310f5..02e4ec3 100644 --- a/api/server/services/submodel_registry_server_service.py +++ b/api/server/services/submodel_registry_server_service.py @@ -1,4 +1,4 @@ -from typing import Any, MutableMapping, List +from typing import Any, MutableMapping, List, Union from aas_core3 import jsonization from aas_core3.types import Submodel, ConceptDescription @@ -40,7 +40,7 @@ def GetAllSubmodelDescriptors(self) -> List[str]: return [jsonization.to_jsonable(descriptor) for descriptor in submodel_descriptors_store] def GetSubmodelDescriptorById(self, descriptor_id) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: try: aas_descriptor = self.obj_store.get_identifiable(descriptor_id) except KeyError as e: diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index 710328a..7695bf7 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -35,7 +35,7 @@ def _get_submodel_element_by_id_short(self, submodel_id, element_id_short): # Endpoint specific logic def get_all_submodels_as_jsonables(self) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: return self._jsonable_submodels(self._get_all_submodels()) def add_submodel_from_body(self, json): From 5ea5797486c9976ec134061cc0803446c3ce570f Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Wed, 26 Feb 2025 14:55:59 +0100 Subject: [PATCH 28/45] Stuff --- api/README.md | 114 +++++++++--------- api/server/routes/aas.py | 73 +++++++---- api/server/routes/aasx_file_server.py | 2 +- api/server/routes/submodel.py | 114 ++++++++++-------- api/server/server.py | 13 +- api/server/services/aas_service.py | 85 +++++++++++++ api/server/services/aasservice.py | 6 - ...service.py => aasx_file_server_service.py} | 0 api/server/services/submodel_service.py | 30 +++-- api/server/utils/decorator.py | 28 +++++ api/server/utils/pagination.py | 7 ++ 11 files changed, 321 insertions(+), 151 deletions(-) create mode 100644 api/server/services/aas_service.py delete mode 100644 api/server/services/aasservice.py rename api/server/services/{aasx_flie_server_service.py => aasx_file_server_service.py} (100%) create mode 100644 api/server/utils/decorator.py create mode 100644 api/server/utils/pagination.py diff --git a/api/README.md b/api/README.md index 9534f0c..ff5f4db 100644 --- a/api/README.md +++ b/api/README.md @@ -14,65 +14,67 @@ Below is the status table for the endpoints, organized as specified. ## AAS Service -| Endpoint | Operation | Description | Status | -|------------------------------------|-----------|-------------------------------------------------|--------| -| `/aas` | GET | Returns the Asset Administration Shell | 📅 | -| `/aas` | PUT | Replaces the current Asset Administration Shell | 📅 | -| `/aas/submodel-refs` | GET | Returns all Submodel References | 📅 | -| `/aas/submodel-refs` | POST | Creates a Submodel Reference | 📅 | -| `/aas/submodel-refs/{submodel_id}` | DELETE | Deletes a Submodel Reference | 📅 | -| `/aas/asset-information` | GET | Returns the Asset Information | 📅 | -| `/aas/asset-information` | PUT | Replaces the Asset Information | 📅 | -| `/aas/asset-information/thumbnail` | GET | Returns the thumbnail file | 📅 | -| `/aas/asset-information/thumbnail` | PUT | Replaces the thumbnail file | 📅 | -| `/aas/asset-information/thumbnail` | DELETE | Deletes the thumbnail file | 📅 | +| Endpoint | Operation | Description | Status | +|-------------------------------------------------------|-----------|------------------------------------------------------------------------|--------| +| `/shells` | GET | Returns all Asset Administration Shells | 📅 | +| `/shells` | POST | Creates a new Asset Administration Shell | 📅 | +| `/shells/$reference` | GET | Returns all Asset Administration Shell references | 📅 | +| `/shells/{aasIdentifier}` | GET | Returns an Asset Administration Shell by ID | 📅 | +| `/shells/{aasIdentifier}` | PUT | Updates an existing Asset Administration Shell | 📅 | +| `/shells/{aasIdentifier}` | DELETE | Deletes an Asset Administration Shell | 📅 | +| `/shells/{aasIdentifier}/$reference` | GET | Returns the reference of a specific Asset Administration Shell | 📅 | +| `/shells/{aasIdentifier}/asset-information` | GET | Returns the Asset Information of a specific Asset Administration Shell | 📅 | +| `/shells/{aasIdentifier}/asset-information` | PUT | Updates the Asset Information of a specific Asset Administration Shell | 📅 | +| `/shells/{aasIdentifier}/asset-information/thumbnail` | GET | Returns the thumbnail file of the Asset Information | 📅 | +| `/shells/{aasIdentifier}/asset-information/thumbnail` | PUT | Replaces the thumbnail file of the Asset Information | 📅 | +| `/shells/{aasIdentifier}/asset-information/thumbnail` | DELETE | Deletes the thumbnail file of the Asset Information | 📅 | ## Submodel Service -| Endpoint | Operation | Description | Status | -|--------------------------------------------------------------------------------------|-----------|------------------------------------------------------------------------------|--------| -| `/submodels/` | GET | Retrieve all submodels | ✅ | -| `/submodels/` | POST | Create a new submodel | ✅ | -| `/submodels/$metadata` | GET | Retrieve metadata for all submodels | 📅 | -| `/submodels/$reference` | GET | Retrieve reference for all submodels | 📅 | -| `/submodels/$value` | GET | Retrieve values of all submodels | ❌ | -| `/submodels/$path` | GET | Retrieve submodels by a specific path | ❌ | -| `/submodels/{submodel_id}` | GET | Retrieve a submodel by ID | ✅ | -| `/submodels/{submodel_id}` | PUT | Update a submodel by ID | ✅ | -| `/submodels/{submodel_id}` | DELETE | Delete a submodel by ID | ✅ | -| `/submodels/{submodel_id}/$metadata` | GET | Retrieve metadata of a specific submodel | 📅 | -| `/submodels/{submodel_id}/$metadata` | PATCH | Update metadata of a specific submodel | 📅 | -| `/submodels/{submodel_id}/$value` | GET | Retrieve values of a specific submodel | ❌ | -| `/submodels/{submodel_id}/$reference` | GET | Retrieve reference of a specific submodel | 📅 | -| `/submodels/{submodel_id}/$path` | GET | Retrieve a specific submodel by path | ❌ | -| `/submodels/{submodel_id}/submodel-elements` | GET | Retrieve all elements of a specific submodel | ✅ | -| `/submodels/{submodel_id}/submodel-elements` | POST | Create new elements in a specific submodel | ✅ | -| `/submodels/{submodel_id}/submodel-elements/$metadata` | GET | Retrieve metadata for submodel elements | 📅 | -| `/submodels/{submodel_id}/submodel-elements/$reference` | GET | Retrieve references for submodel elements | 📅 | -| `/submodels/{submodel_id}/submodel-elements/$value` | GET | Retrieve values for submodel elements | ❌ | -| `/submodels/{submodel_id}/submodel-elements/$path` | GET | Retrieve elements by path in a specific submodel | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | GET | Retrieve specific elements by short ID in a submodel | ✅ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | POST | Create specific elements by short ID in a submodel | ✅ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PUT | Update specific elements by short ID in a submodel | ✅ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | Delete specific elements by short ID in a submodel | ✅ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PATCH | Partially update specific elements by short ID in a submodel | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | GET | Retrieve metadata of specific elements by short ID | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | PATCH | Update metadata of specific elements by short ID | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$reference` | GET | Retrieve reference of specific elements by short ID | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$value` | GET | Retrieve values of specific elements by short ID | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/$value` | PATCH | Update values of specific elements by short ID | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | GET | Retrieve attachments of specific elements by short ID | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | PUT | Update attachments of specific elements by short ID | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | DELETE | Delete attachments of specific elements by short ID | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke` | POST | Invoke operations on specific elements by short ID | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke/$value` | POST | Invoke operations with value on specific elements by short ID | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke-async` | POST | Asynchronously invoke operations on specific elements by short ID | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke-async/$value` | POST | Asynchronously invoke operations with value on specific elements by short ID | ❌ | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | GET | Retrieve qualifiers for specific elements by short ID | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | POST | Add qualifiers to specific elements by short ID | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | GET | Retrieve qualifiers of a specific type for specific elements by short ID | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | PUT | Update qualifiers of a specific type for specific elements by short ID | 📅 | -| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | DELETE | Delete qualifiers of a specific type for specific elements by short ID | 📅 | +| Endpoint | Operation | Description | Status | +|---------------------------------------------------------------------------------------------------|-----------|------------------------------------------------------------------------------|--------| +| `/shells/{aasIdentifier}/submodel-refs` | GET | Retrieve all submodels | ✅ | +| `/shells/{aasIdentifier}/submode-refs` | POST | Create a new submodel | ✅ | +| `/shells/{aasIdentifier}/$metadata` | GET | Retrieve metadata for all submodels | 📅 | +| `/shells/{aasIdentifier}/$reference` | GET | Retrieve reference for all submodels | 📅 | +| `/shells/{aasIdentifier}/$value` | GET | Retrieve values of all submodels | ❌ | +| `/shells/{aasIdentifier}/$path` | GET | Retrieve submodels by a specific path | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}` | GET | Retrieve a submodel by ID | ✅ | +| `/shells/{aasIdentifier}/{submodel_id}` | PUT | Update a submodel by ID | ✅ | +| `/shells/{aasIdentifier}/{submodel_id}` | DELETE | Delete a submodel by ID | ✅ | +| `/shells/{aasIdentifier}/{submodel_id}/$metadata` | GET | Retrieve metadata of a specific submodel | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/$metadata` | PATCH | Update metadata of a specific submodel | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/$value` | GET | Retrieve values of a specific submodel | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/$reference` | GET | Retrieve reference of a specific submodel | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/$path` | GET | Retrieve a specific submodel by path | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements` | GET | Retrieve all elements of a specific submodel | ✅ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements` | POST | Create new elements in a specific submodel | ✅ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/$metadata` | GET | Retrieve metadata for submodel elements | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/$reference` | GET | Retrieve references for submodel elements | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/$value` | GET | Retrieve values for submodel elements | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/$path` | GET | Retrieve elements by path in a specific submodel | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | GET | Retrieve specific elements by short ID in a submodel | ✅ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | POST | Create specific elements by short ID in a submodel | ✅ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | PUT | Update specific elements by short ID in a submodel | ✅ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | Delete specific elements by short ID in a submodel | ✅ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | PATCH | Partially update specific elements by short ID in a submodel | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | GET | Retrieve metadata of specific elements by short ID | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | PATCH | Update metadata of specific elements by short ID | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$reference` | GET | Retrieve reference of specific elements by short ID | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$value` | GET | Retrieve values of specific elements by short ID | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$value` | PATCH | Update values of specific elements by short ID | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/attachment` | GET | Retrieve attachments of specific elements by short ID | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/attachment` | PUT | Update attachments of specific elements by short ID | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/attachment` | DELETE | Delete attachments of specific elements by short ID | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke` | POST | Invoke operations on specific elements by short ID | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke/$value` | POST | Invoke operations with value on specific elements by short ID | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke-async` | POST | Asynchronously invoke operations on specific elements by short ID | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke-async/$value` | POST | Asynchronously invoke operations with value on specific elements by short ID | ❌ | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | GET | Retrieve qualifiers for specific elements by short ID | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | POST | Add qualifiers to specific elements by short ID | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | GET | Retrieve qualifiers of a specific type for specific elements by short ID | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | PUT | Update qualifiers of a specific type for specific elements by short ID | 📅 | +| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | DELETE | Delete qualifiers of a specific type for specific elements by short ID | 📅 | ## AASX File Server Interface and Operations | Endpoint | Operation | Description | Status | diff --git a/api/server/routes/aas.py b/api/server/routes/aas.py index 3d62a55..42b3ab2 100644 --- a/api/server/routes/aas.py +++ b/api/server/routes/aas.py @@ -1,25 +1,52 @@ from typing import Any -from fastapi import APIRouter - - -router = APIRouter() - -@router.get("/aas") -async def get_all_aas() -> Any: - return {"message": ""} - - -@router.get("/aas/{aas_id}") -async def get_aas_by_id(aas_id: str) -> Any: - return {"message": ""} - - -@router.post("/aas") -async def create_aas() -> Any: - return {"message": ""} - - -@router.delete("/aas/{aas_id}") -async def delete_aas(aas_id: str) -> Any: - return {"message": ""} +from aas_core3.types import Identifiable +from fastapi import APIRouter, Request + +from api.server.services.aas_service import AasService +from api.server.utils.pagination import Pagination +from sdk.basyx import ObjectStore + + +class AasRouter(Pagination): + def __init__(self, global_obj_store: ObjectStore[Identifiable]): + self.router = APIRouter() + self.service = AasService(global_obj_store) + self._setup_routes() + + def _setup_routes(self): + @self.router.get("/shells") + async def get_all_aas() -> Any: + return self.service.get_all_shells_as_jsonable() + + @self.router.post("/shells") + async def create_aas(request: Request) -> Any: + body = await request.json() + return self.service.add_shell_from_body(body) + + @self.router.get("/shells/$reference") + async def get_all_aas_reference() -> Any: + return {"message": ""} + + @self.router.get("/shells/{aasIdentifier}") + async def get_aas_by_id(aasIdentifier: str) -> Any: + return {"message": ""} + + @self.router.put("/shells/{aasIdentifier}") + async def update_aas(aasIdentifier: str) -> Any: + return {"message": ""} + + @self.router.delete("/shells/{aasIdentifier}") + async def delete_aas(aasIdentifier: str) -> Any: + return {"message": ""} + + @self.router.get("/shells/{aasIdentifier}/$reference") + async def get_aas_reference_by_id(aasIdentifier: str) -> Any: + return {"message": ""} + + # TODO: Asset-information endpoints + # /shells/{aasIdentifier}/asset-information GET + # /shells/{aasIdentifier}/asset-information PUT + # /shells/{aasIdentifier}/asset-information/thumbnail GET + # /shells/{aasIdentifier}/asset-information/thumbnail PUT + # /shells/{aasIdentifier}/asset-information/thumbnail DELETE diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index 4a1f625..9ffda8a 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -3,7 +3,7 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request -from api.server.services.aasx_flie_server_service import AasxFileServerService +from api.server.services.aasx_file_server_service import AasxFileServerService from sdk.basyx import ObjectStore diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index 741d4f7..7f81d03 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -1,13 +1,16 @@ -from typing import Any +from typing import Any, Iterable from aas_core3.types import Identifiable from fastapi import APIRouter, Request, HTTPException from api.server.services.submodel_service import SubmodelService +from api.server.utils.decorator import limited from sdk.basyx import ObjectStore +from api.server.utils.pagination import Pagination -class SubmodelRouter: + +class SubmodelRouter(Pagination): def __init__(self, global_obj_store: ObjectStore[Identifiable]): self.router = APIRouter() self.obj_store = global_obj_store @@ -15,172 +18,176 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]): self._setup_routes() def _setup_routes(self): - @self.router.get("/") - async def get_submodel_all() -> Any: - return self.service.get_all_submodels_as_jsonables() - - @self.router.post("/") + # TODO: Following the swaggerhub documentation this should only return refs (currently not resolvable?) + # FIXME: Camelcasing? + @self.router.get("/shells/{aasIdentifier}/submodel-refs") + @limited() + async def get_submodel_all(request: Request, aasIdentifier: str) -> Any: + return self.service.get_all_submodels_as_jsonables(aasIdentifier) + + @self.router.post("/shells/{aasIdentifier}/submodel-refs") async def post_submodel(request: Request) -> Any: body = await request.json() return self.service.add_submodel_from_body(body) - @self.router.get("/$metadata") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$metadata") async def get_submodel_all_metadata() -> Any: # Returns metadata for all submodels, stripped of detailed content raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/$reference") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$reference") async def get_submodel_all_reference() -> Any: # Returns references for all submodels without full data raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/$value") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$value") async def not_implemented_value() -> Any: raise HTTPException(status_code=501, detail="This route is not implemented!") - @self.router.get("/$path") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$path") async def not_implemented_path() -> Any: raise HTTPException(status_code=501, detail="This route is not implemented!") - @self.router.get("/{submodel_id}") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}") async def get_submodel(submodel_id: str) -> Any: return self.service.get_submodel_jsonable_by_id(submodel_id) - @self.router.put("/{submodel_id}") + @self.router.put("/shells/{aasIdentifier}/submodels/{submodelIdentifier}") async def put_submodel(submodel_id: str, request: Request) -> Any: # Update submodel with given id body = await request.json() return self.service.update_submodel_by_id(submodel_id, body) - @self.router.delete("/{submodel_id}") - async def delete_submodel(submodel_id: str) -> Any: - return self.service.delete_submodel_by_id(submodel_id) - - @self.router.patch("/{submodel_id}") + @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}") async def not_implemented_patch_submodel(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not implemented!") - # Nested routes for each submodel - @self.router.get("/{submodel_id}/$metadata") + @self.router.delete("/shells/{aasIdentifier}/submodel-refs") + async def delete_submodel(submodel_id: str) -> Any: + return self.service.delete_submodel_by_id(submodel_id) + + # Nested routes for each submodel (PUT/PATCH x $metadata/$value/$reference/$path) + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$metadata") async def get_submodels_metadata(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.patch("/{submodel_id}/$metadata") + @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$metadata") async def not_implemented_metadata_patch(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/$value") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$value") async def not_implemented_value_get(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.patch("/{submodel_id}/$value") + @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$value") async def not_implemented_value_patch(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is yet implemented!") - @self.router.get("/{submodel_id}/$reference") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$reference") async def get_submodels_reference(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/$path") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$path") async def not_implemented_path_get(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements") async def get_submodel_submodel_elements(submodel_id: str) -> Any: # Get submodel elements self.service.get_submodel_elements(submodel_id) - @self.router.post("/{submodel_id}/submodel-elements") + @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements") async def post_submodel_elements(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/$metadata") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/$metadata") async def get_submodel_submodel_elements_metadata(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/$reference") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/$reference") async def get_submodel_submodel_elements_reference(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/$value") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/$value") async def not_implemented_submodel_elements_value(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/$path") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/$path") async def not_implemented_submodel_elements_path(submodel_id: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") async def get_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: return self.service.get_submodel_element(submodel_id, id_shorts) - @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}") + @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") async def post_submodel_submodel_elements_id_short_path(submodel_id: str, request: Request) -> Any: body = await request.json() return self.service.post_submodel_element(submodel_id, body) - @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}") + @self.router.put("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") async def put_submodel_submodel_elements_id_short_path(submodel_id: str, request: Request) -> Any: body = await request.json() return self.service.put_submodel_element(submodel_id, body) - @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}") - async def delete_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - return self.service.delete_submodel_element(submodel_id, id_shorts) - - @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}") + @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") async def not_implemented_patch_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/$metadata") + @self.router.delete("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") + async def delete_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: + return self.service.delete_submodel_element(submodel_id, id_shorts) + + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$metadata") async def get_submodel_submodel_elements_id_short_path_metadata(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}/$metadata") + @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$metadata") async def not_implemented_metadata_patch(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/$reference") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$reference") async def get_submodel_submodel_elements_id_short_path_reference(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/$value") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$value") async def not_implemented_value_get(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.patch("/{submodel_id}/submodel-elements/{id_shorts}/$value") + @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$value") async def not_implemented_value_patch(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/attachment") + @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment") async def get_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}/attachment") + @self.router.put("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment") async def put_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}/attachment") + @self.router.delete("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment") async def delete_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke") + @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke") async def not_implemented_invoke(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke/$value") + @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke/$value") async def not_implemented_invoke_value(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke-async") + @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async") async def not_implemented_invoke_async(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/invoke-async/$value") + @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async/$value") async def not_implemented_invoke_async_value(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") + # FIXME: No updated paths as qualifier endpoints are not present in swaggerhub @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers") async def get_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") @@ -203,3 +210,8 @@ async def put_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: async def delete_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str, qualifier_type: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") + + # FIXME: Missing based on swaggerhub: + # - /shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-status/{handleId} + # - /shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-results/{handleId} + # - /shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/operation-results/{handleId}/$value diff --git a/api/server/server.py b/api/server/server.py index 193733c..63788a8 100644 --- a/api/server/server.py +++ b/api/server/server.py @@ -1,7 +1,7 @@ import uvicorn from fastapi import FastAPI -from api.server.routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server +from api.server.routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server, aas from sdk.basyx import object_store app = FastAPI() @@ -9,6 +9,7 @@ central_object_store = object_store.ObjectStore() +aas_router = aas.AasRouter(central_object_store) submodel_router = submodel.SubmodelRouter(central_object_store) aasx_file_router = aasx_file_server.AasxFileServerRouter(central_object_store) aas_registry_router = aas_registry_server.AasRegistryRouter(central_object_store) @@ -16,10 +17,12 @@ # Register router # TODO: This can be done dynamically based on startup params -app.include_router(submodel_router.router, prefix=prefix + "/submodels") -app.include_router(aasx_file_router.router, prefix=prefix + "/aasx") -app.include_router(aas_registry_router.router, prefix=prefix + "/aas_registry") -app.include_router(submodel_registry_router.router, prefix=prefix + "/submodel_registry") +app.include_router(submodel_router.router, prefix=prefix) +app.include_router(aasx_file_router.router, prefix=prefix) +app.include_router(aas_registry_router.router, prefix=prefix + "/blub") +app.include_router(submodel_registry_router.router, prefix=prefix) +# FIXME: Collision issues +app.include_router(aas_router.router, prefix=prefix + "/test") # Start the server if this file is executed directly if __name__ == "__main__": diff --git a/api/server/services/aas_service.py b/api/server/services/aas_service.py new file mode 100644 index 0000000..9fcfca0 --- /dev/null +++ b/api/server/services/aas_service.py @@ -0,0 +1,85 @@ +from typing import List, Type, Any, MutableMapping, Union + +from aas_core3 import types, jsonization +from aas_core3.types import AssetAdministrationShell +from aas_core3.types import * # TODO: Remove (test input only) +from fastapi import HTTPException + +from sdk.basyx import ObjectStore + + +class AasService: + def __init__(self, global_object_store: ObjectStore): + self.obj_store = global_object_store + + # General helper functions + def _get_all_shells(self) -> List[Type]: + return self.obj_store.get_identifiables_by_type(AssetAdministrationShell) + + def _jsonable_shells(self, submodels: list[AssetAdministrationShell]) \ + -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + return [jsonization.to_jsonable(submodel) for submodel in submodels] + + # Endpoint specific logic + def get_all_shells_as_jsonable(self) -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: + return self._jsonable_shells(self._get_all_shells()) + + def add_shell_from_body(self, json): + shell = jsonization.asset_administration_shell_from_jsonable(json) + try: + self.obj_store.add(shell) + except KeyError as e: + # TODO: Provide a stacktrace + # Wenn anders in Spezifikation, Stacktrace in server log + raise HTTPException(status_code=400, detail=str(e)) + return {"message": "Shell processed"} + + def add_test_input(self): + aas = AssetAdministrationShell(id="urn:x-test:aas1", + asset_information=AssetInformation(asset_kind=AssetKind.TYPE)) + + some_element = Property( + id_short="some_property", + value_type=DataTypeDefXSD.INT, + value="1984" + ) + + another_element = Blob( + id_short="some_blob", + content_type="application/octet-stream", + value=b'\xDE\xAD\xBE\xEF' + ) + + list_element = Blob( + id_short="list_1", + content_type="application/octet-stream", + value=b'\xDE\xAD\xBE\xEF' + ) + + another_list_element = Blob( + id_short="list_2", + content_type="application/octet-stream", + value=b'\xDE\xAD\xBE\xEF' + ) + + element_list = SubmodelElementList(id_short='ExampleSubmodelList', + type_value_list_element=AASSubmodelElements. + SUBMODEL_ELEMENT_LIST, + value=[list_element, another_list_element]) + + submodel1 = Submodel( + id="urn:x-test:submodel1", + submodel_elements=[ + some_element, + another_element, + element_list + ] + ) + submodel2 = Submodel( + id="urn:x-test:submodel2", + submodel_elements=[ + some_element + ] + ) + + self.obj_store.add(aas) diff --git a/api/server/services/aasservice.py b/api/server/services/aasservice.py deleted file mode 100644 index 240f800..0000000 --- a/api/server/services/aasservice.py +++ /dev/null @@ -1,6 +0,0 @@ -from sdk.basyx import ObjectStore - - -class AasService: - def __init__(self, global_object_store: ObjectStore): - self.obj_store = global_object_store diff --git a/api/server/services/aasx_flie_server_service.py b/api/server/services/aasx_file_server_service.py similarity index 100% rename from api/server/services/aasx_flie_server_service.py rename to api/server/services/aasx_file_server_service.py diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index bedb917..8ddf06f 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -1,7 +1,7 @@ -from typing import Any, MutableMapping +from typing import Any, MutableMapping, List, Union, Type from aas_core3 import jsonization -from aas_core3.types import Submodel, SubmodelElement +from aas_core3.types import Submodel, SubmodelElement, AssetAdministrationShell from fastapi import HTTPException from sdk.basyx import ObjectStore @@ -12,8 +12,16 @@ def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store # General helper functions - def _get_all_submodels(self) -> list[Submodel]: - return [item for item in self.obj_store if isinstance(item, Submodel)] + def _get_all_submodels(self) -> List[Type]: + return self.obj_store.get_identifiables_by_type(Submodel) + + def _get_all_submodel_references_by_shell(self, aasIdentifier: str) -> List[Type]: + shell = self.obj_store.get(aasIdentifier) + if isinstance(shell, AssetAdministrationShell): + print("Submodel found") + return shell.submodels + else: + raise HTTPException(status_code=404, detail="AAS " + aasIdentifier + " not found") def _jsonable_submodels(self, submodels: list[Submodel]) \ -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: @@ -34,9 +42,10 @@ def _get_submodel_element_by_id_short(self, submodel_id, element_id_short): return None # Endpoint specific logic - def get_all_submodels_as_jsonables(self) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: - return self._jsonable_submodels(self._get_all_submodels()) + def get_all_submodels_as_jsonables(self, aasIdentifier: str) \ + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: + # FIXME: Apply AAS Ident + return self._jsonable_submodels(self._get_all_submodel_references_by_shell(aasIdentifier)) def add_submodel_from_body(self, json): submodel = jsonization.submodel_from_jsonable(json) @@ -67,6 +76,7 @@ def delete_submodel_by_id(self, submodel_id): return {"message": "Submodel with id " + submodel_id + " deleted successfully"} def get_submodel_elements(self, submodel_id): + # FIXME: Has to respect hierarchy!! submodel = self._get_submodel_by_id(submodel_id) elements = [] for element in submodel.descend(): @@ -98,14 +108,16 @@ def post_submodel_element(self, submodel_id, body): submodel.submodel_elements.append(submodel_element) return jsonization.to_jsonable(submodel_element) else: - raise HTTPException(status_code=400, detail="Submodel element with id " + submodel_element.id_short + " already exists") + raise HTTPException(status_code=400, + detail="Submodel element with id " + submodel_element.id_short + " already exists") def put_submodel_element(self, submodel_id, body): submodel = self._get_submodel_by_id(submodel_id) submodel_element = jsonization.submodel_element_from_jsonable(body) existing_submodel_element = self._get_submodel_element_by_id_short(submodel_id, submodel_element.id_short) if existing_submodel_element is None: - raise HTTPException(status_code=404, detail="Submodel element with id " + submodel_element.id_short + " does not exist") + raise HTTPException(status_code=404, + detail="Submodel element with id " + submodel_element.id_short + " does not exist") else: submodel.submodel_elements.remove(existing_submodel_element) submodel.submodel_elements.append(submodel_element) diff --git a/api/server/utils/decorator.py b/api/server/utils/decorator.py new file mode 100644 index 0000000..54e3638 --- /dev/null +++ b/api/server/utils/decorator.py @@ -0,0 +1,28 @@ + +from functools import wraps +from fastapi import Request +from typing import Callable, Any, List, Union + +def limited(default_limit: int = 100): + def decorator(func: Callable): + @wraps(func) + async def wrapper(*args, **kwargs) -> Any: + # Extract request from kwargs (FastAPI automatically passes it) + request: Request = kwargs.get("request") + limit: Union[str, int] = request.query_params.get("limit", default_limit) + + try: + limit = int(limit) # Ensure limit is an integer + except ValueError: + limit = default_limit # Fallback if conversion fails + + # Call the original function + result = await func(*args, **kwargs) + + # Apply limit if the result is a list + if isinstance(result, list): + return result[:limit] + return result # If not a list, return as-is + + return wrapper + return decorator diff --git a/api/server/utils/pagination.py b/api/server/utils/pagination.py new file mode 100644 index 0000000..46047fa --- /dev/null +++ b/api/server/utils/pagination.py @@ -0,0 +1,7 @@ +from typing import Iterable, Any + + +class Pagination: + # TODO: Add cursor + def paginate(self, collection: Iterable[Any], limit: int) -> Iterable[Any]: + return list(collection)[:limit] \ No newline at end of file From 9d75cf2f133418d2e6c6a2926963eeefc48105f1 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sat, 1 Mar 2025 19:00:06 +0100 Subject: [PATCH 29/45] Finish AAS, Update tests, Update URLs --- api/README.md | 77 ++++--- api/server/routes/aas.py | 22 +- api/server/routes/aas_registry_server.py | 20 +- api/server/routes/aasx_file_server.py | 20 +- api/server/routes/submodel.py | 212 +++++++++--------- api/server/routes/submodel_registry_server.py | 20 +- api/server/server.py | 11 +- .../services/aas_registry_server_service.py | 10 +- api/server/services/aas_service.py | 65 ++---- .../services/aasx_file_server_service.py | 10 +- .../submodel_registry_server_service.py | 10 +- api/server/services/submodel_service.py | 9 +- api/server/utils/decorator.py | 5 +- api/server/utils/pagination.py | 2 +- api/test/examples/aas.json | 7 + api/test/examples/submodel.json | 37 +++ api/test/examples/submodel_modified.json | 37 +++ api/test/test_server.py | 181 ++++++++------- 18 files changed, 402 insertions(+), 353 deletions(-) create mode 100644 api/test/examples/aas.json create mode 100644 api/test/examples/submodel.json create mode 100644 api/test/examples/submodel_modified.json diff --git a/api/README.md b/api/README.md index ff5f4db..7ef4b09 100644 --- a/api/README.md +++ b/api/README.md @@ -11,65 +11,48 @@ > [!warning] > The project is WIP and endpoints might be declared as 'Implemented' whilst still having issues. -Below is the status table for the endpoints, organized as specified. +> Operation Parameters (e.g. level, content, extent) are generally not supported at the moment. + + +Below is the status table for the endpoints, organized as specified. Content parameters (/$reference, /$metadata, etc.) +will be implemented as separate routes, but are not listed in this table as it's a simple suffix and does only affect +serialization settings. ## AAS Service -| Endpoint | Operation | Description | Status | -|-------------------------------------------------------|-----------|------------------------------------------------------------------------|--------| -| `/shells` | GET | Returns all Asset Administration Shells | 📅 | -| `/shells` | POST | Creates a new Asset Administration Shell | 📅 | -| `/shells/$reference` | GET | Returns all Asset Administration Shell references | 📅 | -| `/shells/{aasIdentifier}` | GET | Returns an Asset Administration Shell by ID | 📅 | -| `/shells/{aasIdentifier}` | PUT | Updates an existing Asset Administration Shell | 📅 | -| `/shells/{aasIdentifier}` | DELETE | Deletes an Asset Administration Shell | 📅 | -| `/shells/{aasIdentifier}/$reference` | GET | Returns the reference of a specific Asset Administration Shell | 📅 | -| `/shells/{aasIdentifier}/asset-information` | GET | Returns the Asset Information of a specific Asset Administration Shell | 📅 | -| `/shells/{aasIdentifier}/asset-information` | PUT | Updates the Asset Information of a specific Asset Administration Shell | 📅 | -| `/shells/{aasIdentifier}/asset-information/thumbnail` | GET | Returns the thumbnail file of the Asset Information | 📅 | -| `/shells/{aasIdentifier}/asset-information/thumbnail` | PUT | Replaces the thumbnail file of the Asset Information | 📅 | -| `/shells/{aasIdentifier}/asset-information/thumbnail` | DELETE | Deletes the thumbnail file of the Asset Information | 📅 | +| Endpoint | Operation | Description | Status | +|-----------------------------------------------------------|-----------|------------------------------------------------------------------------|--------| +| `/aas/shells` | GET | Returns all Asset Administration Shells | ✅ | +| `/aas/shells` | POST | Creates a new Asset Administration Shell | ✅ | +| `/aas/shells/{aasIdentifier}` | GET | Returns an Asset Administration Shell by ID | ✅ | +| `/aas/shells/{aasIdentifier}` | PUT | Updates an existing Asset Administration Shell | 📅 | +| `/aas/shells/{aasIdentifier}` | DELETE | Deletes an Asset Administration Shell | ✅ | +| `/aas/shells/{aasIdentifier}/asset-information` | GET | Returns the Asset Information of a specific Asset Administration Shell | 📅 | +| `/aas/shells/{aasIdentifier}/asset-information` | PUT | Updates the Asset Information of a specific Asset Administration Shell | 📅 | +| `/aas/shells/{aasIdentifier}/asset-information/thumbnail` | GET | Returns the thumbnail file of the Asset Information | 📅 | +| `/aas/shells/{aasIdentifier}/asset-information/thumbnail` | PUT | Replaces the thumbnail file of the Asset Information | 📅 | +| `/aas/shells/{aasIdentifier}/asset-information/thumbnail` | DELETE | Deletes the thumbnail file of the Asset Information | 📅 | ## Submodel Service | Endpoint | Operation | Description | Status | |---------------------------------------------------------------------------------------------------|-----------|------------------------------------------------------------------------------|--------| | `/shells/{aasIdentifier}/submodel-refs` | GET | Retrieve all submodels | ✅ | -| `/shells/{aasIdentifier}/submode-refs` | POST | Create a new submodel | ✅ | -| `/shells/{aasIdentifier}/$metadata` | GET | Retrieve metadata for all submodels | 📅 | -| `/shells/{aasIdentifier}/$reference` | GET | Retrieve reference for all submodels | 📅 | -| `/shells/{aasIdentifier}/$value` | GET | Retrieve values of all submodels | ❌ | -| `/shells/{aasIdentifier}/$path` | GET | Retrieve submodels by a specific path | ❌ | +| `/shells/{aasIdentifier}/submodel-refs` | POST | Create a new submodel | ✅ | | `/shells/{aasIdentifier}/{submodel_id}` | GET | Retrieve a submodel by ID | ✅ | | `/shells/{aasIdentifier}/{submodel_id}` | PUT | Update a submodel by ID | ✅ | | `/shells/{aasIdentifier}/{submodel_id}` | DELETE | Delete a submodel by ID | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}/$metadata` | GET | Retrieve metadata of a specific submodel | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/$metadata` | PATCH | Update metadata of a specific submodel | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/$value` | GET | Retrieve values of a specific submodel | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/$reference` | GET | Retrieve reference of a specific submodel | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/$path` | GET | Retrieve a specific submodel by path | ❌ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements` | GET | Retrieve all elements of a specific submodel | ✅ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements` | POST | Create new elements in a specific submodel | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/$metadata` | GET | Retrieve metadata for submodel elements | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/$reference` | GET | Retrieve references for submodel elements | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/$value` | GET | Retrieve values for submodel elements | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/$path` | GET | Retrieve elements by path in a specific submodel | ❌ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | GET | Retrieve specific elements by short ID in a submodel | ✅ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | POST | Create specific elements by short ID in a submodel | ✅ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | PUT | Update specific elements by short ID in a submodel | ✅ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | Delete specific elements by short ID in a submodel | ✅ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | PATCH | Partially update specific elements by short ID in a submodel | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | GET | Retrieve metadata of specific elements by short ID | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$metadata` | PATCH | Update metadata of specific elements by short ID | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$reference` | GET | Retrieve reference of specific elements by short ID | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$value` | GET | Retrieve values of specific elements by short ID | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/$value` | PATCH | Update values of specific elements by short ID | ❌ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/attachment` | GET | Retrieve attachments of specific elements by short ID | 📅 | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/attachment` | PUT | Update attachments of specific elements by short ID | 📅 | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/attachment` | DELETE | Delete attachments of specific elements by short ID | 📅 | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke` | POST | Invoke operations on specific elements by short ID | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke/$value` | POST | Invoke operations with value on specific elements by short ID | ❌ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke-async` | POST | Asynchronously invoke operations on specific elements by short ID | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke-async/$value` | POST | Asynchronously invoke operations with value on specific elements by short ID | ❌ | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | GET | Retrieve qualifiers for specific elements by short ID | 📅 | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | POST | Add qualifiers to specific elements by short ID | 📅 | | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | GET | Retrieve qualifiers of a specific type for specific elements by short ID | 📅 | @@ -137,4 +120,24 @@ Below is the status table for the endpoints, organized as specified. | `/concept-descriptions/{concept_id}` | GET | Returns a Concept Description by ID | 📅 | | `/concept-descriptions/{concept_id}` | POST | Creates a new Concept Description | 📅 | | `/concept-descriptions/{concept_id}` | PUT | Updates a Concept Description by ID | 📅 | -| `/concept-descriptions/{concept_id}` | DELETE | Deletes a Concept Description by ID | 📅 | \ No newline at end of file +| `/concept-descriptions/{concept_id}` | DELETE | Deletes a Concept Description by ID | 📅 | + + +## SerializationModifiers +### Level +| Value | Status | +|-------|--------| +| Deep | ❌ | +| Core | ❌ | +### Content +| Value | Status | +|-----------|--------| +| Normal | ❌ | +| Reference | ❌ | +| Value | ❌ | +| Path | ❌ | +### Extent +| Value | Status | +|------------------|--------| +| WithoutBLOBValue | ❌ | +| WithBLOBValue | ❌ | \ No newline at end of file diff --git a/api/server/routes/aas.py b/api/server/routes/aas.py index 42b3ab2..d7b2475 100644 --- a/api/server/routes/aas.py +++ b/api/server/routes/aas.py @@ -26,22 +26,22 @@ async def create_aas(request: Request) -> Any: @self.router.get("/shells/$reference") async def get_all_aas_reference() -> Any: - return {"message": ""} + return {"message": "Content parameters are not supported yet."} - @self.router.get("/shells/{aasIdentifier}") - async def get_aas_by_id(aasIdentifier: str) -> Any: - return {"message": ""} + @self.router.get("/shells/{aas_identifier}") + async def get_aas_by_id(aas_identifier: str) -> Any: + return self.service.get_shell_jsonable_by_id(aas_identifier) - @self.router.put("/shells/{aasIdentifier}") - async def update_aas(aasIdentifier: str) -> Any: + @self.router.put("/shells/{aas_identifier}") + async def update_aas(aas_identifier: str) -> Any: return {"message": ""} - @self.router.delete("/shells/{aasIdentifier}") - async def delete_aas(aasIdentifier: str) -> Any: - return {"message": ""} + @self.router.delete("/shells/{aas_identifier}") + async def delete_aas(aas_identifier: str) -> Any: + return self.service.delete_shell_by_id(aas_identifier) - @self.router.get("/shells/{aasIdentifier}/$reference") - async def get_aas_reference_by_id(aasIdentifier: str) -> Any: + @self.router.get("/shells/{aas_identifier}/$reference") + async def get_aas_reference_by_id(aas_identifier: str) -> Any: return {"message": ""} # TODO: Asset-information endpoints diff --git a/api/server/routes/aas_registry_server.py b/api/server/routes/aas_registry_server.py index b659ee3..4738ad0 100644 --- a/api/server/routes/aas_registry_server.py +++ b/api/server/routes/aas_registry_server.py @@ -16,23 +16,23 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]): def _setup_routes(self): @self.router.get("/") - async def GetAllAssetAdministrationShellDescriptors() -> Any: - return self.service.GetAllAssetAdministrationShellDescriptors() + async def get_all_aas_descriptors() -> Any: + return self.service.get_all_asset_administration_shell_descriptors() @self.router.get("/{aas_descriptor_id}") - async def GetAssetAdministrationShellDescriptorById(aas_descriptor_id: str) -> Any: - return self.service.GetAssetAdministrationShellDescriptorById(aas_descriptor_id) + async def get_aas_descriptor_by_id(aas_descriptor_id: str) -> Any: + return self.service.get_asset_administration_shell_descriptor_by_id(aas_descriptor_id) @self.router.post("/") - async def PostAssetAdministrationShellDescriptor(request: Request) -> Any: + async def post_aas_descriptor(request: Request) -> Any: body = await request.json() - return self.service.PostAssetAdministrationShellDescriptor(body) + return self.service.post_asset_administration_shell_descriptor(body) @self.router.put("/") - async def PutAssetAdministrationShellDescriptorById(request: Request) -> Any: + async def put_aas_descriptor(request: Request) -> Any: body = await request.json() - return self.service.PutAssetAdministrationShellDescriptorById(body) + return self.service.put_asset_administration_shell_descriptor_by_id(body) @self.router.delete("/{aas_descriptor_id}") - async def DeleteAssetAdministrationShellDescriptorById(aas_descriptor_id: str) -> Any: - return self.service.DeleteAssetAdministrationShellDescriptorById(aas_descriptor_id) + async def delete_aas_descriptor_by_id(aas_descriptor_id: str) -> Any: + return self.service.delete_asset_administration_shell_descriptor_by_id(aas_descriptor_id) diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index 9ffda8a..888ac61 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -16,24 +16,24 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]): def _setup_routes(self): @self.router.get("/") - async def GetAllAASXPackageIds() -> Any: - return self.service.GetAllAASXPackageIds() + async def get_all_aasx() -> Any: + return self.service.get_all_aasx_package_ids() @self.router.get("/{aasx_package_id}") - async def GetAASXByPackageId(aasx_package_id: str) -> Any: - return self.service.GetAASXByPackageId(aasx_package_id) + async def get_aasx_by_package_id(aasx_package_id: str) -> Any: + return self.service.get_aasx_by_package_id(aasx_package_id) @self.router.post("/") - async def PostAASXPackage(request: Request) -> Any: + async def post_aasx_package(request: Request) -> Any: body = await request.json() - return self.service.PostAASXPackage(body) + return self.service.post_aasx_package(body) @self.router.put("/") - async def PutAASXByPackageId(request: Request) -> Any: + async def put_assx_package(request: Request) -> Any: body = await request.json() - return self.service.PutAASXByPackageId(body) + return self.service.put_aasx_by_package_id(body) @self.router.delete("/{aasx_package_id}") - async def DeleteAASXByPackageId(aasx_package_id: str) -> Any: - return self.service.DeleteAASXByPackageId(aasx_package_id) + async def delete_aasx_package_by_id(aasx_package_id: str) -> Any: + return self.service.delete_aasx_by_package_id(aasx_package_id) \ No newline at end of file diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index 7f81d03..7a1da3a 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -18,196 +18,198 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]): self._setup_routes() def _setup_routes(self): - # TODO: Following the swaggerhub documentation this should only return refs (currently not resolvable?) - # FIXME: Camelcasing? - @self.router.get("/shells/{aasIdentifier}/submodel-refs") + # GetAllSubmodels and path-suffixes + @self.router.get("") @limited() - async def get_submodel_all(request: Request, aasIdentifier: str) -> Any: - return self.service.get_all_submodels_as_jsonables(aasIdentifier) + async def get_submodel_all(request: Request) -> Any: + return self.service.get_all_submodels_as_jsonables() - @self.router.post("/shells/{aasIdentifier}/submodel-refs") - async def post_submodel(request: Request) -> Any: - body = await request.json() - return self.service.add_submodel_from_body(body) - - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$metadata") - async def get_submodel_all_metadata() -> Any: + @self.router.get("/$metadata") + @limited() + async def get_submodel_all_metadata(request: Request) -> Any: # Returns metadata for all submodels, stripped of detailed content raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$reference") - async def get_submodel_all_reference() -> Any: + @self.router.get("/$reference") + @limited() + async def get_submodel_all_reference(request: Request) -> Any: # Returns references for all submodels without full data raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$value") - async def not_implemented_value() -> Any: + @self.router.get("/$value") + @limited() + async def not_implemented_value(request: Request) -> Any: raise HTTPException(status_code=501, detail="This route is not implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$path") + @self.router.get("/$path") async def not_implemented_path() -> Any: raise HTTPException(status_code=501, detail="This route is not implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}") - async def get_submodel(submodel_id: str) -> Any: - return self.service.get_submodel_jsonable_by_id(submodel_id) + @self.router.post("") + async def post_submodel(request: Request) -> Any: + body = await request.json() + return self.service.add_submodel_from_body(body) - @self.router.put("/shells/{aasIdentifier}/submodels/{submodelIdentifier}") - async def put_submodel(submodel_id: str, request: Request) -> Any: + @self.router.get("/{submodel_identifier}") + async def get_submodel(submodel_identifier: str) -> Any: + return self.service.get_submodel_jsonable_by_id(submodel_identifier) + + @self.router.put("/{submodel_identifier}") + async def put_submodel(submodel_identifier: str, request: Request) -> Any: # Update submodel with given id body = await request.json() - return self.service.update_submodel_by_id(submodel_id, body) + return self.service.update_submodel_by_id(submodel_identifier, body) - @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}") - async def not_implemented_patch_submodel(submodel_id: str) -> Any: + @self.router.patch("/{submodel_identifier}") + async def not_implemented_patch_submodel(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not implemented!") - @self.router.delete("/shells/{aasIdentifier}/submodel-refs") - async def delete_submodel(submodel_id: str) -> Any: - return self.service.delete_submodel_by_id(submodel_id) + @self.router.delete("/{submodel_identifier}") + async def delete_submodel(submodel_identifier: str) -> Any: + return self.service.delete_submodel_by_id(submodel_identifier) # Nested routes for each submodel (PUT/PATCH x $metadata/$value/$reference/$path) - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$metadata") - async def get_submodels_metadata(submodel_id: str) -> Any: + @self.router.get("/{submodel_identifier}/$metadata") + async def get_submodels_metadata(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$metadata") - async def not_implemented_metadata_patch(submodel_id: str) -> Any: + @self.router.patch("/{submodel_identifier}/$metadata") + async def not_implemented_metadata_patch(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$value") - async def not_implemented_value_get(submodel_id: str) -> Any: + @self.router.get("/{submodel_identifier}/$value") + async def not_implemented_value_get(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$value") - async def not_implemented_value_patch(submodel_id: str) -> Any: + @self.router.patch("/{submodel_identifier}/$value") + async def not_implemented_value_patch(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$reference") - async def get_submodels_reference(submodel_id: str) -> Any: + @self.router.get("/{submodel_identifier}/$reference") + async def get_submodels_reference(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/$path") - async def not_implemented_path_get(submodel_id: str) -> Any: + @self.router.get("/{submodel_identifier}/$path") + async def not_implemented_path_get(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements") - async def get_submodel_submodel_elements(submodel_id: str) -> Any: - # Get submodel elements - self.service.get_submodel_elements(submodel_id) - - @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements") - async def post_submodel_elements(submodel_id: str) -> Any: + @self.router.post("/{submodel_identifier}/submodel-elements") + async def post_submodel_elements(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/$metadata") - async def get_submodel_submodel_elements_metadata(submodel_id: str) -> Any: + @self.router.get("/{submodel_identifier}/submodel-elements") + async def get_submodel_submodel_elements(submodel_identifier: str) -> Any: + # Get submodel elements + self.service.get_submodel_elements(submodel_identifier) + + # GetSubmodelElement and path-suffixes + @self.router.get("/{submodel_identifier}/submodel-elements/$metadata") + async def get_submodel_submodel_elements_metadata(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/$reference") - async def get_submodel_submodel_elements_reference(submodel_id: str) -> Any: + @self.router.get("/{submodel_identifier}/submodel-elements/$reference") + async def get_submodel_submodel_elements_reference(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/$value") - async def not_implemented_submodel_elements_value(submodel_id: str) -> Any: + @self.router.get("/{submodel_identifier}/submodel-elements/$value") + async def not_implemented_submodel_elements_value(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/$path") - async def not_implemented_submodel_elements_path(submodel_id: str) -> Any: + @self.router.get("/{submodel_identifier}/submodel-elements/$path") + async def not_implemented_submodel_elements_path(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") - async def get_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - return self.service.get_submodel_element(submodel_id, id_shorts) + @self.router.get("/{submodel_identifier}/submodel-elements/{id_short_path}") + async def get_submodel_submodel_elements_id_short_path(submodel_identifier: str, id_short_path: str) -> Any: + return self.service.get_submodel_element(submodel_identifier, id_short_path) - @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") - async def post_submodel_submodel_elements_id_short_path(submodel_id: str, request: Request) -> Any: + @self.router.post("/{submodel_identifier}/submodel-elements/{id_short_path}") + async def post_submodel_submodel_elements_id_short_path(submodel_identifier: str, request: Request) -> Any: body = await request.json() - return self.service.post_submodel_element(submodel_id, body) + return self.service.post_submodel_element(submodel_identifier, body) - @self.router.put("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") - async def put_submodel_submodel_elements_id_short_path(submodel_id: str, request: Request) -> Any: + @self.router.put("/{submodel_identifier}/submodel-elements/{id_short_path}") + async def put_submodel_submodel_elements_id_short_path(submodel_identifier: str, request: Request) -> Any: body = await request.json() - return self.service.put_submodel_element(submodel_id, body) + return self.service.put_submodel_element(submodel_identifier, body) - @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") - async def not_implemented_patch_submodel_submodel_elements_id_short_path(submodel_id: str, - id_shorts: str) -> Any: + @self.router.patch("/{submodel_identifier}/submodel-elements/{id_short_path}") + async def not_implemented_patch_submodel_submodel_elements_id_short_path(submodel_identifier: str, + id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.delete("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}") - async def delete_submodel_submodel_elements_id_short_path(submodel_id: str, id_shorts: str) -> Any: - return self.service.delete_submodel_element(submodel_id, id_shorts) + @self.router.delete("/{submodel_identifier}/submodel-elements/{id_short_path}") + async def delete_submodel_submodel_elements_id_short_path(submodel_identifier: str, id_short_path: str) -> Any: + return self.service.delete_submodel_element(submodel_identifier, id_short_path) - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$metadata") - async def get_submodel_submodel_elements_id_short_path_metadata(submodel_id: str, id_shorts: str) -> Any: + @self.router.get("/{submodel_identifier}/submodel-elements/{id_short_path}/$metadata") + async def get_submodel_submodel_elements_id_short_path_metadata(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$metadata") - async def not_implemented_metadata_patch(submodel_id: str, id_shorts: str) -> Any: + @self.router.patch("/{submodel_identifier}/submodel-elements/{id_short_path}/$metadata") + async def not_implemented_metadata_patch(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$reference") - async def get_submodel_submodel_elements_id_short_path_reference(submodel_id: str, id_shorts: str) -> Any: + @self.router.get("/{submodel_identifier}/submodel-elements/{id_short_path}/$reference") + async def get_submodel_submodel_elements_id_short_path_reference(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$value") - async def not_implemented_value_get(submodel_id: str, id_shorts: str) -> Any: + @self.router.get("/{submodel_identifier}/submodel-elements/{id_short_path}/$value") + async def not_implemented_value_get(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.patch("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$value") - async def not_implemented_value_patch(submodel_id: str, id_shorts: str) -> Any: + @self.router.patch("/{submodel_identifier}/submodel-elements/{id_short_path}/$value") + async def not_implemented_value_patch(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment") - async def get_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: + @self.router.get("/{submodel_identifier}/submodel-elements/{id_short_path}/attachment") + async def get_submodel_submodel_element_attachment(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.put("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment") - async def put_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: + @self.router.put("/{submodel_identifier}/submodel-elements/{id_short_path}/attachment") + async def put_submodel_submodel_element_attachment(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.delete("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment") - async def delete_submodel_submodel_element_attachment(submodel_id: str, id_shorts: str) -> Any: + @self.router.delete("/{submodel_identifier}/submodel-elements/{id_short_path}/attachment") + async def delete_submodel_submodel_element_attachment(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke") - async def not_implemented_invoke(submodel_id: str, id_shorts: str) -> Any: + @self.router.post("/{submodel_identifier}/submodel-elements/{id_short_path}/invoke") + async def not_implemented_invoke(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke/$value") - async def not_implemented_invoke_value(submodel_id: str, id_shorts: str) -> Any: + @self.router.post("/{submodel_identifier}/submodel-elements/{id_short_path}/invoke/$value") + async def not_implemented_invoke_value(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async") - async def not_implemented_invoke_async(submodel_id: str, id_shorts: str) -> Any: + @self.router.post("/{submodel_identifier}/submodel-elements/{id_short_path}/invoke-async") + async def not_implemented_invoke_async(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.post("/shells/{aasIdentifier}/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke-async/$value") - async def not_implemented_invoke_async_value(submodel_id: str, id_shorts: str) -> Any: + @self.router.post("/{submodel_identifier}/submodel-elements/{id_short_path}/invoke-async/$value") + async def not_implemented_invoke_async_value(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - # FIXME: No updated paths as qualifier endpoints are not present in swaggerhub - @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers") - async def get_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str) -> Any: + @self.router.get("/{submodel_identifier}/submodel-elements/{id_short_path}/qualifiers") + async def get_submodel_submodel_element_qualifiers(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.post("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers") - async def post_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str) -> Any: + @self.router.post("/{submodel_identifier}/submodel-elements/{id_short_path}/qualifiers") + async def post_submodel_submodel_element_qualifiers(submodel_identifier: str, id_short_path: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.get("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}") - async def get_submodel_submodel_element_qualifiers_specific(submodel_id: str, id_shorts: str, + @self.router.get("/{submodel_identifier}/submodel-elements/{id_short_path}/qualifiers/{qualifier_type}") + async def get_submodel_submodel_element_qualifiers_specific(submodel_identifier: str, id_short_path: str, qualifier_type: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.put("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}") - async def put_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str, + @self.router.put("/{submodel_identifier}/submodel-elements/{id_short_path}/qualifiers/{qualifier_type}") + async def put_submodel_submodel_element_qualifiers(submodel_identifier: str, id_short_path: str, qualifier_type: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") - @self.router.delete("/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}") - async def delete_submodel_submodel_element_qualifiers(submodel_id: str, id_shorts: str, + @self.router.delete("/{submodel_identifier}/submodel-elements/{id_short_path}/qualifiers/{qualifier_type}") + async def delete_submodel_submodel_element_qualifiers(submodel_identifier: str, id_short_path: str, qualifier_type: str) -> Any: raise HTTPException(status_code=501, detail="This route is not yet implemented!") diff --git a/api/server/routes/submodel_registry_server.py b/api/server/routes/submodel_registry_server.py index c6af999..800ac6a 100644 --- a/api/server/routes/submodel_registry_server.py +++ b/api/server/routes/submodel_registry_server.py @@ -16,23 +16,23 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]): def _setup_routes(self): @self.router.get("/") - async def GetAllSubmodelDescriptors() -> Any: - return self.service.GetAllSubmodelDescriptors() + async def get_all_submodel_descriptors() -> Any: + return self.service.get_all_submodel_descriptors() @self.router.get("/{submodel_id}") - async def GetSubmodelDescriptorById(submodel_id: str) -> Any: - return self.service.GetSubmodelDescriptorById(submodel_id) + async def get_submodel_descriptor_by_id(submodel_id: str) -> Any: + return self.service.get_submodel_descriptor_by_id(submodel_id) @self.router.post("/") - async def PostSubmodelDescriptor(request: Request) -> Any: + async def post_submodel_descriptor(request: Request) -> Any: body = await request.json() - return self.service.PostSubmodelDescriptor(body) + return self.service.post_submodel_descriptor(body) @self.router.put("/") - async def PutSubmodelDescriptorById(request: Request) -> Any: + async def put_submodel_descriptor_by_id(request: Request) -> Any: body = await request.json() - return self.service.PutSubmodelDescriptorById(body) + return self.service.put_submodel_descriptor_by_id(body) @self.router.delete("/{submodel_id}") - async def DeleteSubmodelDescriptorById(submodel_id: str) -> Any: - return self.service.DeleteSubmodelDescriptorById(submodel_id) + async def delete_submodel_descriptor_by_id(submodel_id: str) -> Any: + return self.service.delete_submodel_descriptor_by_id(submodel_id) diff --git a/api/server/server.py b/api/server/server.py index 63788a8..ab3b259 100644 --- a/api/server/server.py +++ b/api/server/server.py @@ -17,12 +17,11 @@ # Register router # TODO: This can be done dynamically based on startup params -app.include_router(submodel_router.router, prefix=prefix) -app.include_router(aasx_file_router.router, prefix=prefix) -app.include_router(aas_registry_router.router, prefix=prefix + "/blub") -app.include_router(submodel_registry_router.router, prefix=prefix) -# FIXME: Collision issues -app.include_router(aas_router.router, prefix=prefix + "/test") +app.include_router(submodel_router.router, prefix=prefix + "/submodels") +app.include_router(aasx_file_router.router, prefix=prefix + "/aasx") +app.include_router(aas_registry_router.router, prefix=prefix + "/registry") +app.include_router(submodel_registry_router.router, prefix=prefix + "/submodels") +app.include_router(aas_router.router, prefix=prefix + "/aas") # Start the server if this file is executed directly if __name__ == "__main__": diff --git a/api/server/services/aas_registry_server_service.py b/api/server/services/aas_registry_server_service.py index 028365c..a088e20 100644 --- a/api/server/services/aas_registry_server_service.py +++ b/api/server/services/aas_registry_server_service.py @@ -11,7 +11,7 @@ class AasRegistryServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def GetAllAssetAdministrationShellDescriptors(self) -> list[str]: + def get_all_asset_administration_shell_descriptors(self) -> list[str]: all_descriptors = self.obj_store.get_identifiables_by_type(ConceptDescription) print(all_descriptors.__dict__) print(all_descriptors) @@ -37,13 +37,13 @@ def GetAllAssetAdministrationShellDescriptors(self) -> list[str]: pass return [jsonization.to_jsonable(descriptor) for descriptor in aas_descriptors_store] - def GetAssetAdministrationShellDescriptorById(self, descriptor_id) \ + def get_asset_administration_shell_descriptor_by_id(self, descriptor_id) \ -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: aas_descriptor = self.obj_store.get_identifiable(descriptor_id) assert isinstance(aas_descriptor, ConceptDescription) return jsonization.to_jsonable(aas_descriptor) - def PostAssetAdministrationShellDescriptor(self, json): + def post_asset_administration_shell_descriptor(self, json): aas_descriptor = jsonization.concept_description_from_jsonable(json) try: self.obj_store.add(aas_descriptor) @@ -53,7 +53,7 @@ def PostAssetAdministrationShellDescriptor(self, json): raise HTTPException(status_code=400, detail=str(e)) return {"message": "AAS Descriptor processed"} - def PutAssetAdministrationShellDescriptorById(self, json): + def put_asset_administration_shell_descriptor_by_id(self, json): aas_descriptor = jsonization.asset_administration_shell_from_jsonable(json) try: self.obj_store.delete(aas_descriptor.id) # should there be an exception if there is no aasx_package to @@ -65,7 +65,7 @@ def PutAssetAdministrationShellDescriptorById(self, json): raise HTTPException(status_code=400, detail=str(e)) return {"message": "AASX package updated"} - def DeleteAssetAdministrationShellDescriptorById(self, descriptor_id): + def delete_asset_administration_shell_descriptor_by_id(self, descriptor_id): try: self.obj_store.delete(descriptor_id) # should there be an exception if there is no aasx_package to delete? except KeyError as e: diff --git a/api/server/services/aas_service.py b/api/server/services/aas_service.py index 9fcfca0..72b6531 100644 --- a/api/server/services/aas_service.py +++ b/api/server/services/aas_service.py @@ -2,7 +2,6 @@ from aas_core3 import types, jsonization from aas_core3.types import AssetAdministrationShell -from aas_core3.types import * # TODO: Remove (test input only) from fastapi import HTTPException from sdk.basyx import ObjectStore @@ -14,8 +13,15 @@ def __init__(self, global_object_store: ObjectStore): # General helper functions def _get_all_shells(self) -> List[Type]: + # FIXME: Type issues. Should this be casted to List[AAS]? return self.obj_store.get_identifiables_by_type(AssetAdministrationShell) + def _get_shell_by_id(self, aas_identifier) -> AssetAdministrationShell: + shell = self.obj_store.get(aas_identifier) + if shell is None or not isinstance(shell, AssetAdministrationShell): + raise HTTPException(status_code=404, detail="Submodel with id " + aas_identifier + " not found") + return shell + def _jsonable_shells(self, submodels: list[AssetAdministrationShell]) \ -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: return [jsonization.to_jsonable(submodel) for submodel in submodels] @@ -24,6 +30,10 @@ def _jsonable_shells(self, submodels: list[AssetAdministrationShell]) \ def get_all_shells_as_jsonable(self) -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: return self._jsonable_shells(self._get_all_shells()) + def get_shell_jsonable_by_id(self, aas_identifier): + shell = self._get_shell_by_id(aas_identifier) + return jsonization.to_jsonable(shell) + def add_shell_from_body(self, json): shell = jsonization.asset_administration_shell_from_jsonable(json) try: @@ -33,53 +43,8 @@ def add_shell_from_body(self, json): # Wenn anders in Spezifikation, Stacktrace in server log raise HTTPException(status_code=400, detail=str(e)) return {"message": "Shell processed"} - - def add_test_input(self): - aas = AssetAdministrationShell(id="urn:x-test:aas1", - asset_information=AssetInformation(asset_kind=AssetKind.TYPE)) - - some_element = Property( - id_short="some_property", - value_type=DataTypeDefXSD.INT, - value="1984" - ) - - another_element = Blob( - id_short="some_blob", - content_type="application/octet-stream", - value=b'\xDE\xAD\xBE\xEF' - ) - - list_element = Blob( - id_short="list_1", - content_type="application/octet-stream", - value=b'\xDE\xAD\xBE\xEF' - ) - - another_list_element = Blob( - id_short="list_2", - content_type="application/octet-stream", - value=b'\xDE\xAD\xBE\xEF' - ) - - element_list = SubmodelElementList(id_short='ExampleSubmodelList', - type_value_list_element=AASSubmodelElements. - SUBMODEL_ELEMENT_LIST, - value=[list_element, another_list_element]) - - submodel1 = Submodel( - id="urn:x-test:submodel1", - submodel_elements=[ - some_element, - another_element, - element_list - ] - ) - submodel2 = Submodel( - id="urn:x-test:submodel2", - submodel_elements=[ - some_element - ] - ) - self.obj_store.add(aas) + def delete_shell_by_id(self, aas_identifier): + shell = self._get_shell_by_id(aas_identifier) + self.obj_store.discard(shell) + return {"message": "AssetAdministrationShell with id " + aas_identifier + " deleted successfully"} diff --git a/api/server/services/aasx_file_server_service.py b/api/server/services/aasx_file_server_service.py index a9c2d43..f2374d9 100644 --- a/api/server/services/aasx_file_server_service.py +++ b/api/server/services/aasx_file_server_service.py @@ -11,16 +11,16 @@ class AasxFileServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def GetAllAASXPackageIds(self) -> list[str]: + def get_all_aasx_package_ids(self) -> list[str]: return [item.id for item in self.obj_store if isinstance(item, AssetAdministrationShell)] - def GetAASXByPackageId(self, package_id) \ + def get_aasx_by_package_id(self, package_id) \ -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: aasx_package = self.obj_store.get_identifiable(package_id) assert isinstance(aasx_package, AssetAdministrationShell) return jsonization.to_jsonable(aasx_package) - def PostAASXPackage(self, json): + def post_aasx_package(self, json): aasx_package = jsonization.asset_administration_shell_from_jsonable(json) try: self.obj_store.add(aasx_package) @@ -30,7 +30,7 @@ def PostAASXPackage(self, json): raise HTTPException(status_code=400, detail=str(e)) return {"message": "AASX package processed"} - def PutAASXByPackageId(self, json): + def put_aasx_by_package_id(self, json): aasx_package = jsonization.asset_administration_shell_from_jsonable(json) try: self.obj_store.delete(aasx_package.id) # should there be an exception if there is no aasx_package to @@ -42,7 +42,7 @@ def PutAASXByPackageId(self, json): raise HTTPException(status_code=400, detail=str(e)) return {"message": "AASX package updated"} - def DeleteAASXByPackageId(self, package_id): + def delete_aasx_by_package_id(self, package_id): try: self.obj_store.delete(package_id) # should there be an exception if there is no aasx_package to delete? except KeyError as e: diff --git a/api/server/services/submodel_registry_server_service.py b/api/server/services/submodel_registry_server_service.py index 807f617..98f2629 100644 --- a/api/server/services/submodel_registry_server_service.py +++ b/api/server/services/submodel_registry_server_service.py @@ -11,7 +11,7 @@ class SubmodelRegistryServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def GetAllSubmodelDescriptors(self) -> list[str]: + def get_all_submodel_descriptors(self) -> list[str]: #print(self.obj_store.__dict__) all_descriptors = self.obj_store.get_identifiables_by_type(ConceptDescription) @@ -39,7 +39,7 @@ def GetAllSubmodelDescriptors(self) -> list[str]: pass return [jsonization.to_jsonable(descriptor) for descriptor in submodel_descriptors_store] - def GetSubmodelDescriptorById(self, descriptor_id) \ + def get_submodel_descriptor_by_id(self, descriptor_id) \ -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: try: aas_descriptor = self.obj_store.get_identifiable(descriptor_id) @@ -50,7 +50,7 @@ def GetSubmodelDescriptorById(self, descriptor_id) \ assert isinstance(aas_descriptor, ConceptDescription) return jsonization.to_jsonable(aas_descriptor) - def PostSubmodelDescriptor(self, json): + def post_submodel_descriptor(self, json): submodel_descriptor = jsonization.concept_description_from_jsonable(json) # Check if all referenced submodels exist in the obeject_store @@ -77,7 +77,7 @@ def PostSubmodelDescriptor(self, json): "object_store:" + str(e)) return {"message": "Submodel descriptor processed"} - def PutSubmodelDescriptorById(self, json): + def put_submodel_descriptor_by_id(self, json): submodel_descriptor = jsonization.concept_description_from_jsonable(json) # Check if all referenced submodels exist in the obeject_store @@ -104,7 +104,7 @@ def PutSubmodelDescriptorById(self, json): raise HTTPException(status_code=400, detail=str(e)) return {"message": "AASX package updated"} - def DeleteSubmodelDescriptorById(self, descriptor_id): + def delete_submodel_descriptor_by_id(self, descriptor_id): try: self.obj_store.delete(descriptor_id) # should there be an exception if there is no aasx_package to delete? except KeyError as e: diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index 8ddf06f..2b8e35d 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -18,8 +18,7 @@ def _get_all_submodels(self) -> List[Type]: def _get_all_submodel_references_by_shell(self, aasIdentifier: str) -> List[Type]: shell = self.obj_store.get(aasIdentifier) if isinstance(shell, AssetAdministrationShell): - print("Submodel found") - return shell.submodels + return shell.submodels # FIXME: Typing issues, should this be (safe) casted? else: raise HTTPException(status_code=404, detail="AAS " + aasIdentifier + " not found") @@ -29,7 +28,7 @@ def _jsonable_submodels(self, submodels: list[Submodel]) \ def _get_submodel_by_id(self, submodel_id): submodel = self.obj_store.get(submodel_id) - if submodel is None: + if submodel is None or not isinstance(submodel, Submodel): raise HTTPException(status_code=404, detail="Submodel with id " + submodel_id + " not found") return submodel @@ -42,10 +41,10 @@ def _get_submodel_element_by_id_short(self, submodel_id, element_id_short): return None # Endpoint specific logic - def get_all_submodels_as_jsonables(self, aasIdentifier: str) \ + def get_all_submodels_as_jsonables(self) \ -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: # FIXME: Apply AAS Ident - return self._jsonable_submodels(self._get_all_submodel_references_by_shell(aasIdentifier)) + return self._jsonable_submodels(self._get_all_submodels()) def add_submodel_from_body(self, json): submodel = jsonization.submodel_from_jsonable(json) diff --git a/api/server/utils/decorator.py b/api/server/utils/decorator.py index 54e3638..8035b4a 100644 --- a/api/server/utils/decorator.py +++ b/api/server/utils/decorator.py @@ -1,7 +1,7 @@ - from functools import wraps from fastapi import Request -from typing import Callable, Any, List, Union +from typing import Callable, Any, Union + def limited(default_limit: int = 100): def decorator(func: Callable): @@ -25,4 +25,5 @@ async def wrapper(*args, **kwargs) -> Any: return result # If not a list, return as-is return wrapper + return decorator diff --git a/api/server/utils/pagination.py b/api/server/utils/pagination.py index 46047fa..f04fff6 100644 --- a/api/server/utils/pagination.py +++ b/api/server/utils/pagination.py @@ -4,4 +4,4 @@ class Pagination: # TODO: Add cursor def paginate(self, collection: Iterable[Any], limit: int) -> Iterable[Any]: - return list(collection)[:limit] \ No newline at end of file + return list(collection)[:limit] diff --git a/api/test/examples/aas.json b/api/test/examples/aas.json new file mode 100644 index 0000000..4ae4446 --- /dev/null +++ b/api/test/examples/aas.json @@ -0,0 +1,7 @@ +{ + "id": "urn:x-test:aas1", + "assetInformation": { + "assetKind": "Type" + }, + "modelType": "AssetAdministrationShell" +} \ No newline at end of file diff --git a/api/test/examples/submodel.json b/api/test/examples/submodel.json new file mode 100644 index 0000000..b52c719 --- /dev/null +++ b/api/test/examples/submodel.json @@ -0,0 +1,37 @@ +{ + "id": "urn:x-test:submodel1", + "submodelElements": [ + { + "idShort": "some_property", + "valueType": "xs:int", + "value": "1984", + "modelType": "Property" + }, + { + "idShort": "some_blob", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "ExampleSubmodelList", + "typeValueListElement": "SubmodelElementList", + "value": [ + { + "idShort": "list_1", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "list_2", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + } + ], + "modelType": "SubmodelElementList" + } + ], + "modelType": "Submodel" +} \ No newline at end of file diff --git a/api/test/examples/submodel_modified.json b/api/test/examples/submodel_modified.json new file mode 100644 index 0000000..6d2cf71 --- /dev/null +++ b/api/test/examples/submodel_modified.json @@ -0,0 +1,37 @@ +{ + "id": "urn:x-test:submodel1", + "submodelElements": [ + { + "idShort": "some_property", + "valueType": "xs:int", + "value": "8419", + "modelType": "Property" + }, + { + "idShort": "some_blob", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "ExampleSubmodelList", + "typeValueListElement": "SubmodelElementList", + "value": [ + { + "idShort": "list_1", + "value": "481563", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "list_2", + "value": "&/6453=(", + "contentType": "application/octet-stream", + "modelType": "Blob" + } + ], + "modelType": "SubmodelElementList" + } + ], + "modelType": "Submodel" +} \ No newline at end of file diff --git a/api/test/test_server.py b/api/test/test_server.py index 1d3a3e4..a3fbdbd 100644 --- a/api/test/test_server.py +++ b/api/test/test_server.py @@ -1,112 +1,111 @@ +import json import unittest from fastapi.testclient import TestClient from api.server import app -client = TestClient(app) BASE_URL = "/api/v3.0/" + class TestFastAPIEndpoints(unittest.TestCase): + def setUp(self): + self.client = TestClient(app) + with open("./examples/submodel.json", encoding="utf-8") as f: + self.submodel_example = json.load(f) + with open("./examples/aas.json", encoding="utf-8") as f: + self.aas_example = json.load(f) + with open("./examples/submodel_modified.json", encoding="utf-8") as f: + self.test_submodel_modified = json.load(f) + + self.submodel_example_id = self.submodel_example["id"] + self.shell_example_id = self.aas_example["id"] + self.invalid_submodel_id = "some_id" + self.invalid_aas_example_id = "some_other_id" - test_submodel = { - "id": "urn:x-test:submodel1", - "submodelElements": [ - { - "idShort": "some_property", - "valueType": "xs:int", - "value": "1984", - "modelType": "Property" - }, - { - "idShort": "some_blob", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "ExampleSubmodelList", - "typeValueListElement": "SubmodelElementList", - "value": [ - { - "idShort": "list_1", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "list_2", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - } - ], - "modelType": "SubmodelElementList" - } - ], - "modelType": "Submodel" - } - test_submodel_modified = { - "id": "urn:x-test:submodel1", - "submodelElements": [ - { - "idShort": "some_property", - "valueType": "xs:int", - "value": "8419", - "modelType": "Property" - }, - { - "idShort": "some_blob", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "ExampleSubmodelList", - "typeValueListElement": "SubmodelElementList", - "value": [ - { - "idShort": "list_1", - "value": "481563", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "list_2", - "value": "&/6453=(", - "contentType": "application/octet-stream", - "modelType": "Blob" - } - ], - "modelType": "SubmodelElementList" - } - ], - "modelType": "Submodel" - } - test_submode_id = test_submodel["id"] - invalid_submode_id = "some_blob" - - def test_01_(self): - # Test the GET /items/{item_id} endpoint - response = client.get(BASE_URL + "submodels/") + # Test submodel items + def test_get_all_submodels(self): + response = self.client.get(BASE_URL + "submodels") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), []) - def test_02_post_submodel(self): - response = client.post(BASE_URL + "submodels/", json=self.test_submodel) + # Setup + self.client.post(BASE_URL + "submodels", json=self.submodel_example) + + response = self.client.get(BASE_URL + "submodels") self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.submodel_example]) - def test_03_get_submodel_undefined(self): - response_test_undefined = client.get(BASE_URL + "submodels/" + self.invalid_submode_id + "/") - self.assertEqual(response_test_undefined.status_code, 404) + # Teardown + self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id) + + def test_post_submodel(self): + response = self.client.post(BASE_URL + "submodels", json=self.submodel_example) + self.assertEqual(response.status_code, 200) + self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id) + + def test_get_specific_submodel(self): + # Setup + self.client.post(BASE_URL + "submodels", json=self.submodel_example) + + response_test_undefined = self.client.get(BASE_URL + "submodels/" + self.invalid_submodel_id) + response_test_entry = self.client.get(BASE_URL + "submodels/" + self.submodel_example_id) - def test_04_get_submodel(self): - response_test_entry = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/") + self.assertEqual(response_test_undefined.status_code, 404) self.assertEqual(response_test_entry.status_code, 200) - self.assertEqual(response_test_entry.json(), self.test_submodel) + self.assertEqual(response_test_entry.json(), self.submodel_example) + + # Teardown + self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id + "/") + + def test_get_specific_submodel_element(self): + # Setup + self.client.post(BASE_URL + "submodels", json=self.submodel_example) + + response = self.client.get( + BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements/" + "list_1") + self.assertEqual(response.status_code, 200) + + # Teardown + self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id + "/") + + def test_aas_post(self): + response = self.client.post(BASE_URL + "aas/shells", json=self.aas_example) + self.assertEqual(response.status_code, 200) + + # Teardown + self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) + + def test_get_all_shells(self): + response = self.client.get(BASE_URL + "aas/shells") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + # Setup + self.client.post(BASE_URL + "aas/shells", json=self.aas_example) - def test_05_get_submodel_element(self): - response = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/submodel-elements/" + "list_1") + response = self.client.get(BASE_URL + "aas/shells") self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.aas_example]) + + # Teardown + self.client.delete(BASE_URL + "ass/shells/" + self.shell_example_id) + + def test_get_specific_shell(self): + # Setup + self.client.post(BASE_URL + "aas/shells", json=self.aas_example) + + response_test_undefined = self.client.get(BASE_URL + "aas/shells/" + self.invalid_aas_example_id) + response_test_entry = self.client.get(BASE_URL + "aas/shells/" + self.shell_example_id) + + self.assertEqual(response_test_undefined.status_code, 404) + self.assertEqual(response_test_entry.status_code, 200) + self.assertEqual(response_test_entry.json(), self.aas_example) + + # Teardown + self.client.delete(BASE_URL + "submodels/" + self.shell_example_id + "/") + + # FIXME: Technically a test_delete_shell would be added here. Is this really necessary? + if __name__ == "__main__": unittest.main() From 31c124e466e42a272e375b22b08dd59934707c45 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 2 Mar 2025 00:14:44 +0100 Subject: [PATCH 30/45] Fix imports, fix tests --- api/server/routes/aas.py | 6 +- api/server/routes/aasx_file_server.py | 2 +- api/server/routes/submodel.py | 3 +- api/server/server_main.py | 2 +- api/server/services/aas_service.py | 2 +- api/server/services/submodel_service.py | 8 +- api/test/examples/submodels.py | 74 ------------------- .../{test_server.py => test_aas_service.py} | 50 +------------ api/test/test_submodel_service.py | 69 ++++++++++++----- 9 files changed, 66 insertions(+), 150 deletions(-) delete mode 100644 api/test/examples/submodels.py rename api/test/{test_server.py => test_aas_service.py} (53%) diff --git a/api/server/routes/aas.py b/api/server/routes/aas.py index d7b2475..b96d445 100644 --- a/api/server/routes/aas.py +++ b/api/server/routes/aas.py @@ -3,9 +3,9 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request -from api.server.services.aas_service import AasService -from api.server.utils.pagination import Pagination -from sdk.basyx import ObjectStore +from server.services.aas_service import AasService +from server.utils.pagination import Pagination +from basyx import ObjectStore class AasRouter(Pagination): diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index 40a0a24..91fd583 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -3,7 +3,7 @@ from aas_core3.types import Identifiable from fastapi import APIRouter, Request -from server.services.aasx_flie_server_service import AasxFileServerService +from server.services.aasx_file_server_service import AasxFileServerService from basyx import ObjectStore diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index 9023a46..f008473 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -6,7 +6,8 @@ from server.services.submodel_service import SubmodelService from basyx import ObjectStore -from api.server.utils.pagination import Pagination +from server.utils.pagination import Pagination +from server.utils.decorator import limited class SubmodelRouter(Pagination): diff --git a/api/server/server_main.py b/api/server/server_main.py index 12cf623..8b68771 100644 --- a/api/server/server_main.py +++ b/api/server/server_main.py @@ -1,7 +1,7 @@ import uvicorn from fastapi import FastAPI -from server.routes import submodel, aasx_file_server, aas_registry_server, submodel_registry_server +from server.routes import submodel, aas, aasx_file_server, aas_registry_server, submodel_registry_server from basyx import object_store app = FastAPI() diff --git a/api/server/services/aas_service.py b/api/server/services/aas_service.py index 72b6531..6ff4873 100644 --- a/api/server/services/aas_service.py +++ b/api/server/services/aas_service.py @@ -4,7 +4,7 @@ from aas_core3.types import AssetAdministrationShell from fastapi import HTTPException -from sdk.basyx import ObjectStore +from basyx import ObjectStore class AasService: diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index 0274255..d49244f 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -1,4 +1,4 @@ -from typing import Any, MutableMapping, List, Union +from typing import Any, MutableMapping, List, Union, Type from aas_core3 import jsonization from aas_core3.types import Submodel, SubmodelElement, AssetAdministrationShell @@ -15,12 +15,12 @@ def __init__(self, global_object_store: ObjectStore): def _get_all_submodels(self) -> List[Type]: return self.obj_store.get_identifiables_by_type(Submodel) - def _get_all_submodel_references_by_shell(self, aasIdentifier: str) -> List[Type]: - shell = self.obj_store.get(aasIdentifier) + def _get_all_submodel_references_by_shell(self, aas_identifier: str) -> List[Type]: + shell = self.obj_store.get(aas_identifier) if isinstance(shell, AssetAdministrationShell): return shell.submodels # FIXME: Typing issues, should this be (safe) casted? else: - raise HTTPException(status_code=404, detail="AAS " + aasIdentifier + " not found") + raise HTTPException(status_code=404, detail="AAS " + aas_identifier + " not found") def _jsonable_submodels(self, submodels: List[Submodel]) \ -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: diff --git a/api/test/examples/submodels.py b/api/test/examples/submodels.py deleted file mode 100644 index 951880c..0000000 --- a/api/test/examples/submodels.py +++ /dev/null @@ -1,74 +0,0 @@ -test_submodel = { - "id": "urn:x-test:submodel1", - "submodelElements": [ - { - "idShort": "some_property", - "valueType": "xs:int", - "value": "1984", - "modelType": "Property" - }, - { - "idShort": "some_blob", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "ExampleSubmodelList", - "typeValueListElement": "SubmodelElementList", - "value": [ - { - "idShort": "list_1", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "list_2", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - } - ], - "modelType": "SubmodelElementList" - } - ], - "modelType": "Submodel" - } -test_submodel_modified = { - "id": "urn:x-test:submodel1", - "submodelElements": [ - { - "idShort": "some_property", - "valueType": "xs:int", - "value": "8419", - "modelType": "Property" - }, - { - "idShort": "some_blob", - "value": "3q2+7w==", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "ExampleSubmodelList", - "typeValueListElement": "SubmodelElementList", - "value": [ - { - "idShort": "list_1", - "value": "481563", - "contentType": "application/octet-stream", - "modelType": "Blob" - }, - { - "idShort": "list_2", - "value": "&/6453=(", - "contentType": "application/octet-stream", - "modelType": "Blob" - } - ], - "modelType": "SubmodelElementList" - } - ], - "modelType": "Submodel" -} \ No newline at end of file diff --git a/api/test/test_server.py b/api/test/test_aas_service.py similarity index 53% rename from api/test/test_server.py rename to api/test/test_aas_service.py index a3fbdbd..7c67fe9 100644 --- a/api/test/test_server.py +++ b/api/test/test_aas_service.py @@ -2,12 +2,12 @@ import unittest from fastapi.testclient import TestClient -from api.server import app +from server import app BASE_URL = "/api/v3.0/" -class TestFastAPIEndpoints(unittest.TestCase): +class TestAASService(unittest.TestCase): def setUp(self): self.client = TestClient(app) with open("./examples/submodel.json", encoding="utf-8") as f: @@ -22,52 +22,6 @@ def setUp(self): self.invalid_submodel_id = "some_id" self.invalid_aas_example_id = "some_other_id" - # Test submodel items - def test_get_all_submodels(self): - response = self.client.get(BASE_URL + "submodels") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - # Setup - self.client.post(BASE_URL + "submodels", json=self.submodel_example) - - response = self.client.get(BASE_URL + "submodels") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [self.submodel_example]) - - # Teardown - self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id) - - def test_post_submodel(self): - response = self.client.post(BASE_URL + "submodels", json=self.submodel_example) - self.assertEqual(response.status_code, 200) - self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id) - - def test_get_specific_submodel(self): - # Setup - self.client.post(BASE_URL + "submodels", json=self.submodel_example) - - response_test_undefined = self.client.get(BASE_URL + "submodels/" + self.invalid_submodel_id) - response_test_entry = self.client.get(BASE_URL + "submodels/" + self.submodel_example_id) - - self.assertEqual(response_test_undefined.status_code, 404) - self.assertEqual(response_test_entry.status_code, 200) - self.assertEqual(response_test_entry.json(), self.submodel_example) - - # Teardown - self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id + "/") - - def test_get_specific_submodel_element(self): - # Setup - self.client.post(BASE_URL + "submodels", json=self.submodel_example) - - response = self.client.get( - BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements/" + "list_1") - self.assertEqual(response.status_code, 200) - - # Teardown - self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id + "/") - def test_aas_post(self): response = self.client.post(BASE_URL + "aas/shells", json=self.aas_example) self.assertEqual(response.status_code, 200) diff --git a/api/test/test_submodel_service.py b/api/test/test_submodel_service.py index a6cbca1..5d08cd9 100644 --- a/api/test/test_submodel_service.py +++ b/api/test/test_submodel_service.py @@ -1,40 +1,75 @@ import unittest +import json + from fastapi.testclient import TestClient from server import app -from .examples.submodels import test_submodel_modified, test_submodel client = TestClient(app) BASE_URL = "/api/v3.0/" -class TestFastAPIEndpoints(unittest.TestCase): - test_submode_id = test_submodel["id"] - invalid_submode_id = "some_blob" +class TestSubmodelService(unittest.TestCase): + def setUp(self): + self.client = TestClient(app) + with open("./examples/submodel.json", encoding="utf-8") as f: + self.submodel_example = json.load(f) + with open("./examples/aas.json", encoding="utf-8") as f: + self.aas_example = json.load(f) + with open("./examples/submodel_modified.json", encoding="utf-8") as f: + self.test_submodel_modified = json.load(f) + + self.submodel_example_id = self.submodel_example["id"] + self.shell_example_id = self.aas_example["id"] + self.invalid_submodel_id = "some_id" + self.invalid_aas_example_id = "some_other_id" - def test_01_(self): - # Test the GET /items/{item_id} endpoint - response = client.get(BASE_URL + "submodels/") + # Test submodel items + def test_get_all_submodels(self): + response = self.client.get(BASE_URL + "submodels") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), []) - def test_02_post_submodel(self): - response = client.post(BASE_URL + "submodels/", json=test_submodel) + # Setup + self.client.post(BASE_URL + "submodels", json=self.submodel_example) + + response = self.client.get(BASE_URL + "submodels") self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.submodel_example]) - def test_03_get_submodel_undefined(self): - response_test_undefined = client.get(BASE_URL + "submodels/" + self.invalid_submode_id + "/") - self.assertEqual(response_test_undefined.status_code, 404) + # Teardown + self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id) + + def test_post_submodel(self): + response = self.client.post(BASE_URL + "submodels", json=self.submodel_example) + self.assertEqual(response.status_code, 200) + self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id) - def test_04_get_submodel(self): - response_test_entry = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/") + def test_get_specific_submodel(self): + # Setup + self.client.post(BASE_URL + "submodels", json=self.submodel_example) + + response_test_undefined = self.client.get(BASE_URL + "submodels/" + self.invalid_submodel_id) + response_test_entry = self.client.get(BASE_URL + "submodels/" + self.submodel_example_id) + + self.assertEqual(response_test_undefined.status_code, 404) self.assertEqual(response_test_entry.status_code, 200) - self.assertEqual(response_test_entry.json(), test_submodel) + self.assertEqual(response_test_entry.json(), self.submodel_example) - def test_05_get_submodel_element(self): - response = client.get(BASE_URL + "submodels/" + self.test_submode_id + "/submodel-elements/" + "list_1") + # Teardown + self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id + "/") + + def test_get_specific_submodel_element(self): + # Setup + self.client.post(BASE_URL + "submodels", json=self.submodel_example) + + response = self.client.get( + BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements/" + "list_1") self.assertEqual(response.status_code, 200) + # Teardown + self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id + "/") + if __name__ == "__main__": unittest.main() From 8f89954993ee32045f10571dfaa127ecfd51a832 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 2 Mar 2025 00:28:29 +0100 Subject: [PATCH 31/45] api/test: Resolve issues with relative json paths --- .github/workflows/ci.yml | 2 +- api/test/test_aas_service.py | 8 +++++--- api/test/test_aasx_file_server_service.py | 1 - api/test/test_submodel_service.py | 8 +++++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d4fbd8..c9e1c7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: diff --git a/api/test/test_aas_service.py b/api/test/test_aas_service.py index 7c67fe9..44d4a80 100644 --- a/api/test/test_aas_service.py +++ b/api/test/test_aas_service.py @@ -1,3 +1,4 @@ +import os import json import unittest from fastapi.testclient import TestClient @@ -9,12 +10,13 @@ class TestAASService(unittest.TestCase): def setUp(self): + base_path = os.path.dirname(os.path.abspath(__file__)) self.client = TestClient(app) - with open("./examples/submodel.json", encoding="utf-8") as f: + with open(os.path.join(base_path, "examples", "submodel.json"), encoding="utf-8") as f: self.submodel_example = json.load(f) - with open("./examples/aas.json", encoding="utf-8") as f: + with open(os.path.join(base_path, "examples", "aas.json"), encoding="utf-8") as f: self.aas_example = json.load(f) - with open("./examples/submodel_modified.json", encoding="utf-8") as f: + with open(os.path.join(base_path, "examples", "submodel_modified.json"), encoding="utf-8") as f: self.test_submodel_modified = json.load(f) self.submodel_example_id = self.submodel_example["id"] diff --git a/api/test/test_aasx_file_server_service.py b/api/test/test_aasx_file_server_service.py index b59f9a1..b874802 100644 --- a/api/test/test_aasx_file_server_service.py +++ b/api/test/test_aasx_file_server_service.py @@ -3,7 +3,6 @@ from server import app from .examples.aasx_packages import aasx_package_json -from .examples.submodels import test_submodel_modified, test_submodel client = TestClient(app) BASE_URL = "/api/v3.0/" diff --git a/api/test/test_submodel_service.py b/api/test/test_submodel_service.py index 5d08cd9..fdec911 100644 --- a/api/test/test_submodel_service.py +++ b/api/test/test_submodel_service.py @@ -1,3 +1,4 @@ +import os import unittest import json @@ -11,12 +12,13 @@ class TestSubmodelService(unittest.TestCase): def setUp(self): + base_path = os.path.dirname(os.path.abspath(__file__)) self.client = TestClient(app) - with open("./examples/submodel.json", encoding="utf-8") as f: + with open(os.path.join(base_path, "examples", "submodel.json"), encoding="utf-8") as f: self.submodel_example = json.load(f) - with open("./examples/aas.json", encoding="utf-8") as f: + with open(os.path.join(base_path, "examples", "aas.json"), encoding="utf-8") as f: self.aas_example = json.load(f) - with open("./examples/submodel_modified.json", encoding="utf-8") as f: + with open(os.path.join(base_path, "examples", "submodel_modified.json"), encoding="utf-8") as f: self.test_submodel_modified = json.load(f) self.submodel_example_id = self.submodel_example["id"] From c50e183a6ef66182e7cf4cdc4ba52ac144c719a6 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 2 Mar 2025 00:52:40 +0100 Subject: [PATCH 32/45] api/test: Adjust aasx test so every case can be executed independantly --- api/test/examples/aasx.json | 7 +++ api/test/examples/aasx_packages.py | 4 -- api/test/test_aas_service.py | 11 ++--- api/test/test_aasx_file_server_service.py | 54 +++++++++++++++++------ api/test/test_submodel_service.py | 5 +-- 5 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 api/test/examples/aasx.json delete mode 100644 api/test/examples/aasx_packages.py diff --git a/api/test/examples/aasx.json b/api/test/examples/aasx.json new file mode 100644 index 0000000..8e38447 --- /dev/null +++ b/api/test/examples/aasx.json @@ -0,0 +1,7 @@ +{ + "id":"aasx_package_test", + "assetInformation":{ + "assetKind":"Type" + }, + "modelType":"AssetAdministrationShell" +} \ No newline at end of file diff --git a/api/test/examples/aasx_packages.py b/api/test/examples/aasx_packages.py deleted file mode 100644 index bf12187..0000000 --- a/api/test/examples/aasx_packages.py +++ /dev/null @@ -1,4 +0,0 @@ -from aas_core3 import jsonization - -aasx_package_json = {"id": "aasx_package_test", "assetInformation": {"assetKind": "Type"}, "modelType": "AssetAdministrationShell"} -aasx_package = jsonization.asset_administration_shell_from_jsonable(aasx_package_json) diff --git a/api/test/test_aas_service.py b/api/test/test_aas_service.py index 44d4a80..dba51cb 100644 --- a/api/test/test_aas_service.py +++ b/api/test/test_aas_service.py @@ -12,17 +12,12 @@ class TestAASService(unittest.TestCase): def setUp(self): base_path = os.path.dirname(os.path.abspath(__file__)) self.client = TestClient(app) - with open(os.path.join(base_path, "examples", "submodel.json"), encoding="utf-8") as f: - self.submodel_example = json.load(f) + with open(os.path.join(base_path, "examples", "aas.json"), encoding="utf-8") as f: self.aas_example = json.load(f) - with open(os.path.join(base_path, "examples", "submodel_modified.json"), encoding="utf-8") as f: - self.test_submodel_modified = json.load(f) - self.submodel_example_id = self.submodel_example["id"] self.shell_example_id = self.aas_example["id"] - self.invalid_submodel_id = "some_id" - self.invalid_aas_example_id = "some_other_id" + self.invalid_shell_example_id = "some_other_id" def test_aas_post(self): response = self.client.post(BASE_URL + "aas/shells", json=self.aas_example) @@ -50,7 +45,7 @@ def test_get_specific_shell(self): # Setup self.client.post(BASE_URL + "aas/shells", json=self.aas_example) - response_test_undefined = self.client.get(BASE_URL + "aas/shells/" + self.invalid_aas_example_id) + response_test_undefined = self.client.get(BASE_URL + "aas/shells/" + self.invalid_shell_example_id) response_test_entry = self.client.get(BASE_URL + "aas/shells/" + self.shell_example_id) self.assertEqual(response_test_undefined.status_code, 404) diff --git a/api/test/test_aasx_file_server_service.py b/api/test/test_aasx_file_server_service.py index b874802..4e5a065 100644 --- a/api/test/test_aasx_file_server_service.py +++ b/api/test/test_aasx_file_server_service.py @@ -1,36 +1,64 @@ +import os +import json import unittest from fastapi.testclient import TestClient from server import app -from .examples.aasx_packages import aasx_package_json +from aas_core3 import jsonization client = TestClient(app) BASE_URL = "/api/v3.0/" class TestFastAPIEndpoints(unittest.TestCase): - test_aasx_package_id = aasx_package_json["id"] - invalid_submodel_id = "some_blob" + def setUp(self): + base_path = os.path.dirname(os.path.abspath(__file__)) + self.client = TestClient(app) - def test_01_get_all_aasx_package_ids(self): - # Test the GET / endpoint - response = client.get(BASE_URL + "aasx/") + with open(os.path.join(base_path, "examples", "aasx.json"), encoding="utf-8") as f: + self.aasx_json = json.load(f) + + self.test_aasx_id = self.aasx_json["id"] + self.aasx = jsonization.asset_administration_shell_from_jsonable(self.aasx_json) + + def test_get_all_aasx_package_ids(self): + # Test empty + response = self.client.get(BASE_URL + "aasx") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), []) - def test_02_post_aasx_package(self): - response = client.post(BASE_URL + "aasx/", json=aasx_package_json) + # Setup + self.client.post(BASE_URL + "aasx", json=self.aasx_json) + + response = self.client.get(BASE_URL + "aasx") self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.test_aasx_id]) - def test_03_get_aasx_package(self): - response_test_entry = client.get(BASE_URL + "aasx/" + self.test_aasx_package_id + "/") + # Teardown + self.client.delete(BASE_URL + "aasx/" + self.test_aasx_id) + + def test_post_aasx_package(self): + response = client.post(BASE_URL + "aasx/", json=self.aasx_json) + self.assertEqual(response.status_code, 200) + + # Teardown + self.client.delete(BASE_URL + "aasx/" + self.test_aasx_id) + + def test_get_aasx_package(self): + # Setup + self.client.post(BASE_URL + "aasx", json=self.aasx_json) + + response_test_entry = client.get(BASE_URL + "aasx/" + self.test_aasx_id + "/") self.assertEqual(response_test_entry.status_code, 200) - self.assertEqual(response_test_entry.json(), aasx_package_json) + self.assertEqual(response_test_entry.json(), self.aasx_json) + + # Teardown + self.client.delete(BASE_URL + "aasx/" + self.test_aasx_id) - def test_04_delete_aasx_package(self): + def test_delete_aasx_package(self): pass - def test_05_put_aasx_package(self): + def test_put_aasx_package(self): pass diff --git a/api/test/test_submodel_service.py b/api/test/test_submodel_service.py index fdec911..e335627 100644 --- a/api/test/test_submodel_service.py +++ b/api/test/test_submodel_service.py @@ -14,17 +14,14 @@ class TestSubmodelService(unittest.TestCase): def setUp(self): base_path = os.path.dirname(os.path.abspath(__file__)) self.client = TestClient(app) + with open(os.path.join(base_path, "examples", "submodel.json"), encoding="utf-8") as f: self.submodel_example = json.load(f) - with open(os.path.join(base_path, "examples", "aas.json"), encoding="utf-8") as f: - self.aas_example = json.load(f) with open(os.path.join(base_path, "examples", "submodel_modified.json"), encoding="utf-8") as f: self.test_submodel_modified = json.load(f) self.submodel_example_id = self.submodel_example["id"] - self.shell_example_id = self.aas_example["id"] self.invalid_submodel_id = "some_id" - self.invalid_aas_example_id = "some_other_id" # Test submodel items def test_get_all_submodels(self): From 44f57eda8e4eca88327cf1ac700e13b37ce8283d Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 2 Mar 2025 00:56:50 +0100 Subject: [PATCH 33/45] aasx_server: Remove redundant slashes at the end of path to fix unittests --- api/server/routes/aasx_file_server.py | 6 +++--- api/test/test_aasx_file_server_service.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index 91fd583..cdc7bcc 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -15,7 +15,7 @@ def __init__(self, global_obj_store: ObjectStore[Identifiable]): self._setup_routes() def _setup_routes(self): - @self.router.get("/") + @self.router.get("") async def get_all_aasx() -> Any: return self.service.get_all_aasx_package_ids() @@ -23,12 +23,12 @@ async def get_all_aasx() -> Any: async def get_aasx_by_package_id(aasx_package_id: str) -> Any: return self.service.get_aasx_by_package_id(aasx_package_id) - @self.router.post("/") + @self.router.post("") async def post_aasx_package(request: Request) -> Any: body = await request.json() return self.service.post_aasx_package(body) - @self.router.put("/") + @self.router.put("") async def put_assx_package(request: Request) -> Any: body = await request.json() return self.service.put_aasx_by_package_id(body) diff --git a/api/test/test_aasx_file_server_service.py b/api/test/test_aasx_file_server_service.py index 4e5a065..4c5d728 100644 --- a/api/test/test_aasx_file_server_service.py +++ b/api/test/test_aasx_file_server_service.py @@ -38,7 +38,7 @@ def test_get_all_aasx_package_ids(self): self.client.delete(BASE_URL + "aasx/" + self.test_aasx_id) def test_post_aasx_package(self): - response = client.post(BASE_URL + "aasx/", json=self.aasx_json) + response = client.post(BASE_URL + "aasx", json=self.aasx_json) self.assertEqual(response.status_code, 200) # Teardown @@ -48,7 +48,7 @@ def test_get_aasx_package(self): # Setup self.client.post(BASE_URL + "aasx", json=self.aasx_json) - response_test_entry = client.get(BASE_URL + "aasx/" + self.test_aasx_id + "/") + response_test_entry = client.get(BASE_URL + "aasx/" + self.test_aasx_id) self.assertEqual(response_test_entry.status_code, 200) self.assertEqual(response_test_entry.json(), self.aasx_json) From bde3c8d2fd13d8248dc7b1299994d7188ac8f10a Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 2 Mar 2025 01:20:39 +0100 Subject: [PATCH 34/45] api/test/test_aas_service.py: Fix teardown so tests dont influence each other --- api/test/test_aas_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/test_aas_service.py b/api/test/test_aas_service.py index dba51cb..48032cb 100644 --- a/api/test/test_aas_service.py +++ b/api/test/test_aas_service.py @@ -53,7 +53,7 @@ def test_get_specific_shell(self): self.assertEqual(response_test_entry.json(), self.aas_example) # Teardown - self.client.delete(BASE_URL + "submodels/" + self.shell_example_id + "/") + self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) # FIXME: Technically a test_delete_shell would be added here. Is this really necessary? From a822dcb105778e127aa188fef6d55d31b3dc1d73 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sat, 8 Mar 2025 00:26:54 +0100 Subject: [PATCH 35/45] api: Resolve Union and List style affecting python 3.8 compatibility --- api/server/services/aas_registry_server_service.py | 4 ++-- api/server/services/aas_service.py | 4 ++-- api/server/services/aasx_file_server_service.py | 4 ++-- api/server/services/submodel_registry_server_service.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/server/services/aas_registry_server_service.py b/api/server/services/aas_registry_server_service.py index 979dc79..97d126e 100644 --- a/api/server/services/aas_registry_server_service.py +++ b/api/server/services/aas_registry_server_service.py @@ -11,7 +11,7 @@ class AasRegistryServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def get_all_asset_administration_shell_descriptors(self) -> list[str]: + def get_all_asset_administration_shell_descriptors(self) -> List[str]: all_descriptors = self.obj_store.get_identifiables_by_type(ConceptDescription) print(all_descriptors.__dict__) print(all_descriptors) @@ -38,7 +38,7 @@ def get_all_asset_administration_shell_descriptors(self) -> list[str]: return [jsonization.to_jsonable(descriptor) for descriptor in aas_descriptors_store] def get_asset_administration_shell_descriptor_by_id(self, descriptor_id) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: aas_descriptor = self.obj_store.get_identifiable(descriptor_id) assert isinstance(aas_descriptor, ConceptDescription) return jsonization.to_jsonable(aas_descriptor) diff --git a/api/server/services/aas_service.py b/api/server/services/aas_service.py index 6ff4873..9d87251 100644 --- a/api/server/services/aas_service.py +++ b/api/server/services/aas_service.py @@ -22,8 +22,8 @@ def _get_shell_by_id(self, aas_identifier) -> AssetAdministrationShell: raise HTTPException(status_code=404, detail="Submodel with id " + aas_identifier + " not found") return shell - def _jsonable_shells(self, submodels: list[AssetAdministrationShell]) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + def _jsonable_shells(self, submodels: List[AssetAdministrationShell]) \ + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: return [jsonization.to_jsonable(submodel) for submodel in submodels] # Endpoint specific logic diff --git a/api/server/services/aasx_file_server_service.py b/api/server/services/aasx_file_server_service.py index d7731b4..e23c176 100644 --- a/api/server/services/aasx_file_server_service.py +++ b/api/server/services/aasx_file_server_service.py @@ -11,11 +11,11 @@ class AasxFileServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def get_all_aasx_package_ids(self) -> list[str]: + def get_all_aasx_package_ids(self) -> List[str]: return [item.id for item in self.obj_store if isinstance(item, AssetAdministrationShell)] def get_aasx_by_package_id(self, package_id) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: aasx_package = self.obj_store.get_identifiable(package_id) assert isinstance(aasx_package, AssetAdministrationShell) return jsonization.to_jsonable(aasx_package) diff --git a/api/server/services/submodel_registry_server_service.py b/api/server/services/submodel_registry_server_service.py index 4c9900a..4c4bd1c 100644 --- a/api/server/services/submodel_registry_server_service.py +++ b/api/server/services/submodel_registry_server_service.py @@ -11,7 +11,7 @@ class SubmodelRegistryServerService: def __init__(self, global_object_store: ObjectStore): self.obj_store = global_object_store - def get_all_submodel_descriptors(self) -> list[str]: + def get_all_submodel_descriptors(self) -> List[str]: #print(self.obj_store.__dict__) all_descriptors = self.obj_store.get_identifiables_by_type(ConceptDescription) @@ -40,7 +40,7 @@ def get_all_submodel_descriptors(self) -> list[str]: return [jsonization.to_jsonable(descriptor) for descriptor in submodel_descriptors_store] def get_submodel_descriptor_by_id(self, descriptor_id) \ - -> list[bool | int | float | str | list[Any] | MutableMapping[str, Any]]: + -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: try: aas_descriptor = self.obj_store.get_identifiable(descriptor_id) except KeyError as e: From 23b3a05a2ad9daf0b5290786826b4180a302db04 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sat, 8 Mar 2025 18:01:14 +0100 Subject: [PATCH 36/45] api/server/services/aasservice.py: Delete file as its duplicate and empty --- api/server/services/aasservice.py | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 api/server/services/aasservice.py diff --git a/api/server/services/aasservice.py b/api/server/services/aasservice.py deleted file mode 100644 index 88dc49a..0000000 --- a/api/server/services/aasservice.py +++ /dev/null @@ -1,6 +0,0 @@ -from basyx import ObjectStore - - -class AasService: - def __init__(self, global_object_store: ObjectStore): - self.obj_store = global_object_store From 0f9934da4d39373e77fc40608bf3f1152e3893d8 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 9 Mar 2025 00:46:06 +0100 Subject: [PATCH 37/45] /api: Add put shell endpoint and unit test --- api/README.md | 2 +- api/server/routes/aas.py | 6 ++++-- api/server/services/aas_service.py | 11 ++++++++++- api/test/examples/aas_modified.json | 7 +++++++ api/test/test_aas_service.py | 21 +++++++++++++++++++++ 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 api/test/examples/aas_modified.json diff --git a/api/README.md b/api/README.md index 7ef4b09..e992581 100644 --- a/api/README.md +++ b/api/README.md @@ -24,7 +24,7 @@ serialization settings. | `/aas/shells` | GET | Returns all Asset Administration Shells | ✅ | | `/aas/shells` | POST | Creates a new Asset Administration Shell | ✅ | | `/aas/shells/{aasIdentifier}` | GET | Returns an Asset Administration Shell by ID | ✅ | -| `/aas/shells/{aasIdentifier}` | PUT | Updates an existing Asset Administration Shell | 📅 | +| `/aas/shells/{aasIdentifier}` | PUT | Updates an existing Asset Administration Shell | ✅ | | `/aas/shells/{aasIdentifier}` | DELETE | Deletes an Asset Administration Shell | ✅ | | `/aas/shells/{aasIdentifier}/asset-information` | GET | Returns the Asset Information of a specific Asset Administration Shell | 📅 | | `/aas/shells/{aasIdentifier}/asset-information` | PUT | Updates the Asset Information of a specific Asset Administration Shell | 📅 | diff --git a/api/server/routes/aas.py b/api/server/routes/aas.py index b96d445..1f10b42 100644 --- a/api/server/routes/aas.py +++ b/api/server/routes/aas.py @@ -33,8 +33,10 @@ async def get_aas_by_id(aas_identifier: str) -> Any: return self.service.get_shell_jsonable_by_id(aas_identifier) @self.router.put("/shells/{aas_identifier}") - async def update_aas(aas_identifier: str) -> Any: - return {"message": ""} + async def put_aas(aas_identifier: str, request: Request) -> Any: + # Update shell with given id + body = await request.json() + return self.service.put_shell_by_id(aas_identifier, body) @self.router.delete("/shells/{aas_identifier}") async def delete_aas(aas_identifier: str) -> Any: diff --git a/api/server/services/aas_service.py b/api/server/services/aas_service.py index 9d87251..5faf927 100644 --- a/api/server/services/aas_service.py +++ b/api/server/services/aas_service.py @@ -1,6 +1,6 @@ from typing import List, Type, Any, MutableMapping, Union -from aas_core3 import types, jsonization +from aas_core3 import jsonization from aas_core3.types import AssetAdministrationShell from fastapi import HTTPException @@ -44,6 +44,15 @@ def add_shell_from_body(self, json): raise HTTPException(status_code=400, detail=str(e)) return {"message": "Shell processed"} + def put_shell_by_id(self, aas_identifier, json): + shell = self._get_shell_by_id(aas_identifier) + new_shell = jsonization.asset_administration_shell_from_jsonable(json) + if shell.id != new_shell.id: + raise HTTPException(403, "Shell with id " + aas_identifier + " does not match") + self.obj_store.discard(shell) + self.obj_store.add(new_shell) + return jsonization.to_jsonable(new_shell) + def delete_shell_by_id(self, aas_identifier): shell = self._get_shell_by_id(aas_identifier) self.obj_store.discard(shell) diff --git a/api/test/examples/aas_modified.json b/api/test/examples/aas_modified.json new file mode 100644 index 0000000..f68966f --- /dev/null +++ b/api/test/examples/aas_modified.json @@ -0,0 +1,7 @@ +{ + "id": "urn:x-test:aas1", + "assetInformation": { + "assetKind": "Instance" + }, + "modelType": "AssetAdministrationShell" +} \ No newline at end of file diff --git a/api/test/test_aas_service.py b/api/test/test_aas_service.py index 48032cb..bf9fd6d 100644 --- a/api/test/test_aas_service.py +++ b/api/test/test_aas_service.py @@ -16,6 +16,10 @@ def setUp(self): with open(os.path.join(base_path, "examples", "aas.json"), encoding="utf-8") as f: self.aas_example = json.load(f) + # FIXME: modified AAS should contain more complex types but deserialization seems to fail + with open(os.path.join(base_path, "examples", "aas_modified.json"), encoding="utf-8") as f: + self.aas_example_modified = json.load(f) + self.shell_example_id = self.aas_example["id"] self.invalid_shell_example_id = "some_other_id" @@ -55,6 +59,23 @@ def test_get_specific_shell(self): # Teardown self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) + def test_put_shell(self): + # Setup and Preconditions + self.assertNotEqual(self.aas_example, self.aas_example_modified) + self.client.post(BASE_URL + "aas/shells", json=self.aas_example) + + response_normal = self.client.get(BASE_URL + "aas/shells/" + self.shell_example_id) + self.assertEqual(response_normal.json(), self.aas_example) + + response = self.client.put(BASE_URL + "aas/shells/" + self.shell_example_id, json=self.aas_example_modified) + self.assertEqual(response.status_code, 200) + response_overwritten = self.client.get(BASE_URL + "aas/shells/" + self.shell_example_id) + self.assertEqual(response_overwritten.status_code, 200) + self.assertEqual(response_overwritten.json(), self.aas_example_modified) + + # Teardown + self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) + # FIXME: Technically a test_delete_shell would be added here. Is this really necessary? From 3eed6ae691f0cc02ea4cd62c3705aa7e673799fb Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 9 Mar 2025 00:51:41 +0100 Subject: [PATCH 38/45] api/README.md: Update AASX File server status table --- api/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/README.md b/api/README.md index e992581..b97ada9 100644 --- a/api/README.md +++ b/api/README.md @@ -60,13 +60,13 @@ serialization settings. | `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | DELETE | Delete qualifiers of a specific type for specific elements by short ID | 📅 | ## AASX File Server Interface and Operations -| Endpoint | Operation | Description | Status | -|---------------------------|-----------|-------------|--------| -| `/GetAllAASXPackageIds/` | GET | TODO | ✅ | -| `/GetAASXByPackageId/` | POST | TODO | ✅ | -| `/PostAASXPackage/` | POST | TODO | ✅ | -| `/PutAASXByPackageId/` | PUT | TODO | ✅ | -| `/DeleteAASXByPackageId/` | DELETE | TODO | ✅ | +| Endpoint | Operation | Description | Status | +|---------------------------|-----------|----------------------------------------------------|--------| +| `/aasx` | GET | Returns all available AASX packages at the server. | ✅ | +| `/aasx/{aasx_package_id}` | GET | Returns a specific AASX package from the server. | ✅ | +| `/aasx` | POST | Creates an AASX package at the server. | ✅ | +| `/aasx` | PUT | Replaces the AASX package at the server. | ✅ | +| `/aasx/{aasx_package_id}` | DELETE | Deletes a specific AASX package at the server. | ✅ | ## AAS Registry Service | Endpoint | Operation | Description | Status | From 4c5537034b1fe2903d3d53aeaa065f7ee3b53abf Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 9 Mar 2025 11:05:36 +0100 Subject: [PATCH 39/45] /api: Add asset-information endpoints and tests --- api/README.md | 24 ++--- api/server/routes/aas.py | 28 ++++-- api/server/services/aas_service.py | 33 ++++++- api/test/examples/aas.json | 7 -- .../{aas_modified.json => aas/aas.json} | 5 +- api/test/examples/aas/aas_modified.json | 10 +++ api/test/examples/aas/asset_information.json | 6 ++ .../aas/asset_information_modified.json | 6 ++ .../aas/asset_information_no_thumbnail.json | 3 + api/test/examples/aas/thumbnail.json | 3 + api/test/examples/aas/thumbnail_modified.json | 3 + api/test/examples/{ => aasx}/aasx.json | 0 .../examples/{ => submodel}/submodel.json | 0 .../{ => submodel}/submodel_modified.json | 0 api/test/test_aas_service.py | 89 ++++++++++++++++++- api/test/test_aasx_file_server_service.py | 2 +- api/test/test_submodel_service.py | 4 +- 17 files changed, 189 insertions(+), 34 deletions(-) delete mode 100644 api/test/examples/aas.json rename api/test/examples/{aas_modified.json => aas/aas.json} (56%) create mode 100644 api/test/examples/aas/aas_modified.json create mode 100644 api/test/examples/aas/asset_information.json create mode 100644 api/test/examples/aas/asset_information_modified.json create mode 100644 api/test/examples/aas/asset_information_no_thumbnail.json create mode 100644 api/test/examples/aas/thumbnail.json create mode 100644 api/test/examples/aas/thumbnail_modified.json rename api/test/examples/{ => aasx}/aasx.json (100%) rename api/test/examples/{ => submodel}/submodel.json (100%) rename api/test/examples/{ => submodel}/submodel_modified.json (100%) diff --git a/api/README.md b/api/README.md index b97ada9..0f0aefa 100644 --- a/api/README.md +++ b/api/README.md @@ -19,18 +19,18 @@ will be implemented as separate routes, but are not listed in this table as it's serialization settings. ## AAS Service -| Endpoint | Operation | Description | Status | -|-----------------------------------------------------------|-----------|------------------------------------------------------------------------|--------| -| `/aas/shells` | GET | Returns all Asset Administration Shells | ✅ | -| `/aas/shells` | POST | Creates a new Asset Administration Shell | ✅ | -| `/aas/shells/{aasIdentifier}` | GET | Returns an Asset Administration Shell by ID | ✅ | -| `/aas/shells/{aasIdentifier}` | PUT | Updates an existing Asset Administration Shell | ✅ | -| `/aas/shells/{aasIdentifier}` | DELETE | Deletes an Asset Administration Shell | ✅ | -| `/aas/shells/{aasIdentifier}/asset-information` | GET | Returns the Asset Information of a specific Asset Administration Shell | 📅 | -| `/aas/shells/{aasIdentifier}/asset-information` | PUT | Updates the Asset Information of a specific Asset Administration Shell | 📅 | -| `/aas/shells/{aasIdentifier}/asset-information/thumbnail` | GET | Returns the thumbnail file of the Asset Information | 📅 | -| `/aas/shells/{aasIdentifier}/asset-information/thumbnail` | PUT | Replaces the thumbnail file of the Asset Information | 📅 | -| `/aas/shells/{aasIdentifier}/asset-information/thumbnail` | DELETE | Deletes the thumbnail file of the Asset Information | 📅 | +| Endpoint | Operation | Description | Status | +|------------------------------------------------------------|-----------|-------------------------------------------------------------------------|--------| +| `/aas/shells` | GET | Returns all Asset Administration Shells | ✅ | +| `/aas/shells` | POST | Creates a new Asset Administration Shell | ✅ | +| `/aas/shells/{aas_identifier}` | GET | Returns an Asset Administration Shell by ID | ✅ | +| `/aas/shells/{aas_identifier}` | PUT | Updates an existing Asset Administration Shell | ✅ | +| `/aas/shells/{aas_identifier}` | DELETE | Deletes an Asset Administration Shell | ✅ | +| `/aas/shells/{aas_identifier}/asset-information` | GET | Returns the Asset Information of a specific Asset Administration Shell | ✅ | +| `/aas/shells/{aas_identifier}/asset-information` | PUT | Replaces the Asset Information of a specific Asset Administration Shell | ✅ | +| `/aas/shells/{aas_identifier}/asset-information/thumbnail` | GET | Returns the thumbnail file of the Asset Information | ✅ | +| `/aas/shells/{aas_identifier}/asset-information/thumbnail` | PUT | Replaces the thumbnail file of the Asset Information | ✅ | +| `/aas/shells/{aas_identifier}/asset-information/thumbnail` | DELETE | Deletes the thumbnail file of the Asset Information | ✅ | ## Submodel Service diff --git a/api/server/routes/aas.py b/api/server/routes/aas.py index 1f10b42..445b6a0 100644 --- a/api/server/routes/aas.py +++ b/api/server/routes/aas.py @@ -42,13 +42,27 @@ async def put_aas(aas_identifier: str, request: Request) -> Any: async def delete_aas(aas_identifier: str) -> Any: return self.service.delete_shell_by_id(aas_identifier) - @self.router.get("/shells/{aas_identifier}/$reference") + @self.router.get("/shells/{aas_identifier}/asset-information") async def get_aas_reference_by_id(aas_identifier: str) -> Any: - return {"message": ""} + return self.service.get_asset_information_by_id_as_jsonable(aas_identifier) + + @self.router.put("/shells/{aas_identifier}/asset-information") + async def get_aas_reference_by_id(aas_identifier: str, request: Request) -> Any: + body = await request.json() + return self.service.put_asset_information_by_id_from_jsonable(aas_identifier, body) + + @self.router.get("/shells/{aas_identifier}/asset-information/thumbnail") + async def get_aas_thumbnail_by_id(aas_identifier: str) -> Any: + return self.service.get_thumbnail_by_id(aas_identifier) + + @self.router.put("/shells/{aas_identifier}/asset-information/thumbnail") + async def get_aas_reference_by_id(aas_identifier: str, request: Request) -> Any: + body = await request.json() + return self.service.put_thumbnail_by_id(aas_identifier, body) + + @self.router.delete("/shells/{aas_identifier}/asset-information/thumbnail") + async def delete_aas(aas_identifier: str) -> Any: + return self.service.delete_thumbnail_by_id(aas_identifier) # TODO: Asset-information endpoints - # /shells/{aasIdentifier}/asset-information GET - # /shells/{aasIdentifier}/asset-information PUT - # /shells/{aasIdentifier}/asset-information/thumbnail GET - # /shells/{aasIdentifier}/asset-information/thumbnail PUT - # /shells/{aasIdentifier}/asset-information/thumbnail DELETE + # /shells/{aas_identifier}/$reference GET diff --git a/api/server/services/aas_service.py b/api/server/services/aas_service.py index 5faf927..aebb922 100644 --- a/api/server/services/aas_service.py +++ b/api/server/services/aas_service.py @@ -1,7 +1,7 @@ from typing import List, Type, Any, MutableMapping, Union from aas_core3 import jsonization -from aas_core3.types import AssetAdministrationShell +from aas_core3.types import AssetAdministrationShell, AssetInformation from fastapi import HTTPException from basyx import ObjectStore @@ -57,3 +57,34 @@ def delete_shell_by_id(self, aas_identifier): shell = self._get_shell_by_id(aas_identifier) self.obj_store.discard(shell) return {"message": "AssetAdministrationShell with id " + aas_identifier + " deleted successfully"} + + def get_asset_information_by_id_as_jsonable(self, aas_identifier): + shell = self._get_shell_by_id(aas_identifier) + return jsonization.to_jsonable(shell.asset_information) + + def put_asset_information_by_id_from_jsonable(self, aas_identifier, json): + shell = self._get_shell_by_id(aas_identifier) + self.obj_store.discard(shell) + new_information = jsonization.asset_information_from_jsonable(json) + shell.asset_information = new_information + self.obj_store.add(shell) + return jsonization.to_jsonable(shell) + + def get_thumbnail_by_id(self, aas_identifier): + shell = self._get_shell_by_id(aas_identifier) + return jsonization.to_jsonable(shell.asset_information.default_thumbnail) + + def put_thumbnail_by_id(self, aas_identifier, thumbnail): + shell = self._get_shell_by_id(aas_identifier) + self.obj_store.discard(shell) + new_thumbnail = jsonization.resource_from_jsonable(thumbnail) + shell.asset_information.default_thumbnail = new_thumbnail + self.obj_store.add(shell) + return jsonization.to_jsonable(shell.asset_information.default_thumbnail) + + def delete_thumbnail_by_id(self, ass_identifier): + shell = self._get_shell_by_id(ass_identifier) + self.obj_store.discard(shell) + shell.asset_information.default_thumbnail = None + self.obj_store.add(shell) + return jsonization.to_jsonable(shell) \ No newline at end of file diff --git a/api/test/examples/aas.json b/api/test/examples/aas.json deleted file mode 100644 index 4ae4446..0000000 --- a/api/test/examples/aas.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "urn:x-test:aas1", - "assetInformation": { - "assetKind": "Type" - }, - "modelType": "AssetAdministrationShell" -} \ No newline at end of file diff --git a/api/test/examples/aas_modified.json b/api/test/examples/aas/aas.json similarity index 56% rename from api/test/examples/aas_modified.json rename to api/test/examples/aas/aas.json index f68966f..67ed2f6 100644 --- a/api/test/examples/aas_modified.json +++ b/api/test/examples/aas/aas.json @@ -1,7 +1,10 @@ { "id": "urn:x-test:aas1", "assetInformation": { - "assetKind": "Instance" + "assetKind": "Type", + "defaultThumbnail": { + "path": "blub" + } }, "modelType": "AssetAdministrationShell" } \ No newline at end of file diff --git a/api/test/examples/aas/aas_modified.json b/api/test/examples/aas/aas_modified.json new file mode 100644 index 0000000..f5baf9d --- /dev/null +++ b/api/test/examples/aas/aas_modified.json @@ -0,0 +1,10 @@ +{ + "id": "urn:x-test:aas1", + "assetInformation": { + "assetKind": "Instance", + "defaultThumbnail": { + "path": "blub" + } + }, + "modelType": "AssetAdministrationShell" +} \ No newline at end of file diff --git a/api/test/examples/aas/asset_information.json b/api/test/examples/aas/asset_information.json new file mode 100644 index 0000000..6075246 --- /dev/null +++ b/api/test/examples/aas/asset_information.json @@ -0,0 +1,6 @@ +{ + "assetKind": "Type", + "defaultThumbnail": { + "path": "blub" + } +} \ No newline at end of file diff --git a/api/test/examples/aas/asset_information_modified.json b/api/test/examples/aas/asset_information_modified.json new file mode 100644 index 0000000..12574f8 --- /dev/null +++ b/api/test/examples/aas/asset_information_modified.json @@ -0,0 +1,6 @@ +{ + "assetKind": "Instance", + "defaultThumbnail": { + "path": "blub" + } +} \ No newline at end of file diff --git a/api/test/examples/aas/asset_information_no_thumbnail.json b/api/test/examples/aas/asset_information_no_thumbnail.json new file mode 100644 index 0000000..25838a9 --- /dev/null +++ b/api/test/examples/aas/asset_information_no_thumbnail.json @@ -0,0 +1,3 @@ +{ + "assetKind": "Type" +} \ No newline at end of file diff --git a/api/test/examples/aas/thumbnail.json b/api/test/examples/aas/thumbnail.json new file mode 100644 index 0000000..edc4f93 --- /dev/null +++ b/api/test/examples/aas/thumbnail.json @@ -0,0 +1,3 @@ +{ + "path": "blub" +} \ No newline at end of file diff --git a/api/test/examples/aas/thumbnail_modified.json b/api/test/examples/aas/thumbnail_modified.json new file mode 100644 index 0000000..7a615dc --- /dev/null +++ b/api/test/examples/aas/thumbnail_modified.json @@ -0,0 +1,3 @@ +{ + "path": "bluuub" +} \ No newline at end of file diff --git a/api/test/examples/aasx.json b/api/test/examples/aasx/aasx.json similarity index 100% rename from api/test/examples/aasx.json rename to api/test/examples/aasx/aasx.json diff --git a/api/test/examples/submodel.json b/api/test/examples/submodel/submodel.json similarity index 100% rename from api/test/examples/submodel.json rename to api/test/examples/submodel/submodel.json diff --git a/api/test/examples/submodel_modified.json b/api/test/examples/submodel/submodel_modified.json similarity index 100% rename from api/test/examples/submodel_modified.json rename to api/test/examples/submodel/submodel_modified.json diff --git a/api/test/test_aas_service.py b/api/test/test_aas_service.py index bf9fd6d..96028b0 100644 --- a/api/test/test_aas_service.py +++ b/api/test/test_aas_service.py @@ -13,11 +13,26 @@ def setUp(self): base_path = os.path.dirname(os.path.abspath(__file__)) self.client = TestClient(app) - with open(os.path.join(base_path, "examples", "aas.json"), encoding="utf-8") as f: + with open(os.path.join(base_path, "examples/aas", "aas.json"), encoding="utf-8") as f: self.aas_example = json.load(f) + with open(os.path.join(base_path, "examples/aas", "asset_information.json"), encoding="utf-8") as f: + self.asset_information_example = json.load(f) + + with open(os.path.join(base_path, "examples/aas", "asset_information_modified.json"), encoding="utf-8") as f: + self.asset_information_example_modified = json.load(f) + + with open(os.path.join(base_path, "examples/aas", "asset_information_no_thumbnail.json"), encoding="utf-8") as f: + self.asset_information_example_no_thumbnail = json.load(f) + + with open(os.path.join(base_path, "examples/aas", "thumbnail.json"), encoding="utf-8") as f: + self.thumbnail_example = json.load(f) + + with open(os.path.join(base_path, "examples/aas", "thumbnail_modified.json"), encoding="utf-8") as f: + self.thumbnail_example_modified = json.load(f) + # FIXME: modified AAS should contain more complex types but deserialization seems to fail - with open(os.path.join(base_path, "examples", "aas_modified.json"), encoding="utf-8") as f: + with open(os.path.join(base_path, "examples/aas", "aas_modified.json"), encoding="utf-8") as f: self.aas_example_modified = json.load(f) self.shell_example_id = self.aas_example["id"] @@ -76,8 +91,76 @@ def test_put_shell(self): # Teardown self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) - # FIXME: Technically a test_delete_shell would be added here. Is this really necessary? + # FIXME: Technically a test_delete_shell would be added here. Is this really necessary? Kinda covered by the rest + + # Asset Information Endpoints + + def test_get_asset_information(self): + # Setup + self.client.post(BASE_URL + "aas/shells", json=self.aas_example) + + response = self.client.get(BASE_URL + "aas/shells/" + self.shell_example_id + "/asset-information") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), self.asset_information_example) + + # Teardown + self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) + + def test_put_asset_information(self): + # Setup + self.client.post(BASE_URL + "aas/shells", json=self.aas_example) + + # Replace the asset-information converting aas_example to aas_example_modified + # FIXME: This should probably have more complex asset-information to test + response = self.client.put(BASE_URL + "aas/shells/" + self.shell_example_id + "/asset-information", + json=self.asset_information_example_modified) + self.assertEqual(response.status_code, 200) + + response_modified = self.client.get(BASE_URL + "aas/shells/" + self.shell_example_id + "/asset-information") + self.assertEqual(response_modified.status_code, 200) + self.assertEqual(response_modified.json(), self.asset_information_example_modified) + + # Teardown + self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) + + def test_get_asset_information_thumbnail(self): + # Setup + self.client.post(BASE_URL + "aas/shells", json=self.aas_example) + + response = self.client.get(BASE_URL + "aas/shells/" + self.shell_example_id + "/asset-information/thumbnail") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), self.thumbnail_example) + + # Teardown + self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) + + def test_put_asset_information_thumbnail(self): + # Setup + self.client.post(BASE_URL + "aas/shells", json=self.aas_example) + + response = self.client.put(BASE_URL + "aas/shells/" + self.shell_example_id + "/asset-information/thumbnail", + json=self.thumbnail_example_modified) + self.assertEqual(response.status_code, 200) + response_modified = self.client.get( + BASE_URL + "aas/shells/" + self.shell_example_id + "/asset-information/thumbnail") + self.assertEqual(response.status_code, 200) + self.assertEqual(response_modified.json(), self.thumbnail_example_modified) + + # Teardown + self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) + + def test_delete_asset_information_thumbnail(self): + # Setup + self.client.post(BASE_URL + "aas/shells", json=self.aas_example) + + self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id + "/asset-information/thumbnail") + response = self.client.get(BASE_URL + "aas/shells/" + self.shell_example_id + "/asset-information") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), self.asset_information_example_no_thumbnail) + + # Teardown + self.client.delete(BASE_URL + "aas/shells/" + self.shell_example_id) if __name__ == "__main__": unittest.main() diff --git a/api/test/test_aasx_file_server_service.py b/api/test/test_aasx_file_server_service.py index 4c5d728..66905b4 100644 --- a/api/test/test_aasx_file_server_service.py +++ b/api/test/test_aasx_file_server_service.py @@ -15,7 +15,7 @@ def setUp(self): base_path = os.path.dirname(os.path.abspath(__file__)) self.client = TestClient(app) - with open(os.path.join(base_path, "examples", "aasx.json"), encoding="utf-8") as f: + with open(os.path.join(base_path, "examples/aasx", "aasx.json"), encoding="utf-8") as f: self.aasx_json = json.load(f) self.test_aasx_id = self.aasx_json["id"] diff --git a/api/test/test_submodel_service.py b/api/test/test_submodel_service.py index e335627..e526090 100644 --- a/api/test/test_submodel_service.py +++ b/api/test/test_submodel_service.py @@ -15,9 +15,9 @@ def setUp(self): base_path = os.path.dirname(os.path.abspath(__file__)) self.client = TestClient(app) - with open(os.path.join(base_path, "examples", "submodel.json"), encoding="utf-8") as f: + with open(os.path.join(base_path, "examples/submodel", "submodel.json"), encoding="utf-8") as f: self.submodel_example = json.load(f) - with open(os.path.join(base_path, "examples", "submodel_modified.json"), encoding="utf-8") as f: + with open(os.path.join(base_path, "examples/submodel", "submodel_modified.json"), encoding="utf-8") as f: self.test_submodel_modified = json.load(f) self.submodel_example_id = self.submodel_example["id"] From 0a51dcc11004cf629eefd95bf032a9ca1bf82caf Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 9 Mar 2025 15:31:44 +0100 Subject: [PATCH 40/45] api/README.md: Update submodel service endpoint addresses --- api/README.md | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/api/README.md b/api/README.md index 0f0aefa..19940b2 100644 --- a/api/README.md +++ b/api/README.md @@ -34,30 +34,31 @@ serialization settings. ## Submodel Service -| Endpoint | Operation | Description | Status | -|---------------------------------------------------------------------------------------------------|-----------|------------------------------------------------------------------------------|--------| -| `/shells/{aasIdentifier}/submodel-refs` | GET | Retrieve all submodels | ✅ | -| `/shells/{aasIdentifier}/submodel-refs` | POST | Create a new submodel | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}` | GET | Retrieve a submodel by ID | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}` | PUT | Update a submodel by ID | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}` | DELETE | Delete a submodel by ID | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements` | GET | Retrieve all elements of a specific submodel | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements` | POST | Create new elements in a specific submodel | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | GET | Retrieve specific elements by short ID in a submodel | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | POST | Create specific elements by short ID in a submodel | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | PUT | Update specific elements by short ID in a submodel | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | Delete specific elements by short ID in a submodel | ✅ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}` | PATCH | Partially update specific elements by short ID in a submodel | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/attachment` | GET | Retrieve attachments of specific elements by short ID | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/attachment` | PUT | Update attachments of specific elements by short ID | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/attachment` | DELETE | Delete attachments of specific elements by short ID | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke` | POST | Invoke operations on specific elements by short ID | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/invoke-async` | POST | Asynchronously invoke operations on specific elements by short ID | ❌ | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | GET | Retrieve qualifiers for specific elements by short ID | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | POST | Add qualifiers to specific elements by short ID | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | GET | Retrieve qualifiers of a specific type for specific elements by short ID | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | PUT | Update qualifiers of a specific type for specific elements by short ID | 📅 | -| `/shells/{aasIdentifier}/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | DELETE | Delete qualifiers of a specific type for specific elements by short ID | 📅 | +| Endpoint | Operation | Description | Status | +|--------------------------------------------------------------------------------------|-----------|--------------------------------------------------------------------------|--------| +| `/submodels` | GET | Retrieve all submodels | ✅ | +| `/submodels` | POST | Create a new submodel | ✅ | +| `/submodels/{submodel_id}` | GET | Retrieve a submodel by ID | ✅ | +| `/submodels/{submodel_id}` | PUT | Replace a submodel by ID | ✅ | +| `/submodels/{submodel_id}` | PATCH | Update a submodel by ID | 📅 | +| `/submodels/{submodel_id}` | DELETE | Delete a submodel by ID | ✅ | +| `/submodels/{submodel_id}/submodel-elements` | GET | Retrieve all elements of a specific submodel | ✅ | +| `/submodels/{submodel_id}/submodel-elements` | POST | Create new elements in a specific submodel | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | GET | Retrieve specific elements by short ID in a submodel | ✅ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | POST | Create specific elements by short ID in a submodel | ✅ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PUT | Update specific elements by short ID in a submodel | ✅ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | DELETE | Delete specific elements by short ID in a submodel | ✅ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}` | PATCH | Partially update specific elements by short ID in a submodel | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | GET | Retrieve attachments of specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | PUT | Update attachments of specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/attachment` | DELETE | Delete attachments of specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke` | POST | Invoke operations on specific elements by short ID | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/invoke-async` | POST | Asynchronously invoke operations on specific elements by short ID | ❌ | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | GET | Retrieve qualifiers for specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers` | POST | Add qualifiers to specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | GET | Retrieve qualifiers of a specific type for specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | PUT | Update qualifiers of a specific type for specific elements by short ID | 📅 | +| `/submodels/{submodel_id}/submodel-elements/{id_shorts}/qualifiers/{qualifier_type}` | DELETE | Delete qualifiers of a specific type for specific elements by short ID | 📅 | ## AASX File Server Interface and Operations | Endpoint | Operation | Description | Status | From 59a659528ae50c2563000b2af7313613f483808d Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Sun, 9 Mar 2025 20:56:46 +0100 Subject: [PATCH 41/45] /api: Add more tests based on coverage report --- api/server/routes/submodel.py | 8 +-- .../examples/submodel/submodel_element.json | 6 +++ .../submodel/submodel_element_new.json | 6 +++ .../submodel/submodel_with_new_element.json | 43 +++++++++++++++ api/test/test_aasx_file_server_service.py | 1 + api/test/test_submodel_service.py | 52 +++++++++++++++++++ 6 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 api/test/examples/submodel/submodel_element.json create mode 100644 api/test/examples/submodel/submodel_element_new.json create mode 100644 api/test/examples/submodel/submodel_with_new_element.json diff --git a/api/server/routes/submodel.py b/api/server/routes/submodel.py index f008473..4be004d 100644 --- a/api/server/routes/submodel.py +++ b/api/server/routes/submodel.py @@ -94,8 +94,9 @@ async def not_implemented_path_get(submodel_identifier: str) -> Any: raise HTTPException(status_code=501, detail="This route is yet implemented!") @self.router.post("/{submodel_identifier}/submodel-elements") - async def post_submodel_elements(submodel_identifier: str) -> Any: - raise HTTPException(status_code=501, detail="This route is not yet implemented!") + async def post_submodel_elements(submodel_identifier: str, request: Request) -> Any: + body = await request.json() + return self.service.post_submodel_element(submodel_identifier, body) @self.router.get("/{submodel_identifier}/submodel-elements") async def get_submodel_submodel_elements(submodel_identifier: str) -> Any: @@ -125,8 +126,7 @@ async def get_submodel_submodel_elements_id_short_path(submodel_identifier: str, @self.router.post("/{submodel_identifier}/submodel-elements/{id_short_path}") async def post_submodel_submodel_elements_id_short_path(submodel_identifier: str, request: Request) -> Any: - body = await request.json() - return self.service.post_submodel_element(submodel_identifier, body) + raise HTTPException(status_code=501, detail="This route is not yet implemented!") @self.router.put("/{submodel_identifier}/submodel-elements/{id_short_path}") async def put_submodel_submodel_elements_id_short_path(submodel_identifier: str, request: Request) -> Any: diff --git a/api/test/examples/submodel/submodel_element.json b/api/test/examples/submodel/submodel_element.json new file mode 100644 index 0000000..48ef945 --- /dev/null +++ b/api/test/examples/submodel/submodel_element.json @@ -0,0 +1,6 @@ +{ + "idShort": "some_property_123", + "valueType": "xs:int", + "value": "8419", + "modelType": "Property" +} \ No newline at end of file diff --git a/api/test/examples/submodel/submodel_element_new.json b/api/test/examples/submodel/submodel_element_new.json new file mode 100644 index 0000000..48ef945 --- /dev/null +++ b/api/test/examples/submodel/submodel_element_new.json @@ -0,0 +1,6 @@ +{ + "idShort": "some_property_123", + "valueType": "xs:int", + "value": "8419", + "modelType": "Property" +} \ No newline at end of file diff --git a/api/test/examples/submodel/submodel_with_new_element.json b/api/test/examples/submodel/submodel_with_new_element.json new file mode 100644 index 0000000..be7a61c --- /dev/null +++ b/api/test/examples/submodel/submodel_with_new_element.json @@ -0,0 +1,43 @@ +{ + "id": "urn:x-test:submodel1", + "submodelElements": [ + { + "idShort": "some_property", + "valueType": "xs:int", + "value": "1984", + "modelType": "Property" + }, + { + "idShort": "some_blob", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "ExampleSubmodelList", + "typeValueListElement": "SubmodelElementList", + "value": [ + { + "idShort": "list_1", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + }, + { + "idShort": "list_2", + "value": "3q2+7w==", + "contentType": "application/octet-stream", + "modelType": "Blob" + } + ], + "modelType": "SubmodelElementList" + }, + { + "idShort": "some_property_123", + "valueType": "xs:int", + "value": "8419", + "modelType": "Property" + } + ], + "modelType": "Submodel" +} \ No newline at end of file diff --git a/api/test/test_aasx_file_server_service.py b/api/test/test_aasx_file_server_service.py index 66905b4..260837e 100644 --- a/api/test/test_aasx_file_server_service.py +++ b/api/test/test_aasx_file_server_service.py @@ -61,6 +61,7 @@ def test_delete_aasx_package(self): def test_put_aasx_package(self): pass +# FIXME: Add missing tests if __name__ == "__main__": unittest.main() diff --git a/api/test/test_submodel_service.py b/api/test/test_submodel_service.py index e526090..2a86c7e 100644 --- a/api/test/test_submodel_service.py +++ b/api/test/test_submodel_service.py @@ -19,9 +19,16 @@ def setUp(self): self.submodel_example = json.load(f) with open(os.path.join(base_path, "examples/submodel", "submodel_modified.json"), encoding="utf-8") as f: self.test_submodel_modified = json.load(f) + with open(os.path.join(base_path, "examples/submodel", "submodel_element.json"), encoding="utf-8") as f: + self.submodel_element = json.load(f) + with open(os.path.join(base_path, "examples/submodel", "submodel_element_new.json"), encoding="utf-8") as f: + self.submodel_element_new = json.load(f) + with open(os.path.join(base_path, "examples/submodel", "submodel_with_new_element.json"), encoding="utf-8") as f: + self.submodel_with_new_element = json.load(f) self.submodel_example_id = self.submodel_example["id"] self.invalid_submodel_id = "some_id" + self.invalid_submodel_element_id = "some_unknown_element_id" # Test submodel items def test_get_all_submodels(self): @@ -66,9 +73,54 @@ def test_get_specific_submodel_element(self): BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements/" + "list_1") self.assertEqual(response.status_code, 200) + response_none = self.client.get(BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements/" + + "list_undefined") + self.assertEqual(response_none.status_code, 404) + # Teardown self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id + "/") + def test_post_submodel_element(self): + # Setup + self.client.post(BASE_URL + "submodels", json=self.submodel_example) + + response = self.client.post(BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements", + json=self.submodel_element_new) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), self.submodel_element_new) + + + response_already_exists = self.client.post(BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements", + json=self.submodel_element_new) + self.assertEqual(response_already_exists.status_code, 400) + + new_submodel = self.client.get(BASE_URL + "submodels/" + self.submodel_example_id) + self.assertEqual(new_submodel.status_code, 200) + self.assertEqual(new_submodel.json(), self.submodel_with_new_element) + + def test_delete_submodel_element(self): + # Setup + self.client.post(BASE_URL + "submodels", json=self.submodel_example) + self.client.post(BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements", + json=self.submodel_element_new) + + + response = self.client.delete( + BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements/" + "some_property_123") + self.assertEqual(response.status_code, 200) + + response_not_found = self.client.delete( + BASE_URL + "submodels/" + self.submodel_example_id + "/submodel-elements/" + "some_property_unknown") + self.assertEqual(response_not_found.status_code, 404) + + new_submodel = self.client.get(BASE_URL + "submodels/" + self.submodel_example_id) + self.assertEqual(new_submodel.status_code, 200) + self.assertEqual(new_submodel.json(), self.submodel_example) + + # Teardown + self.client.delete(BASE_URL + "submodels/" + self.submodel_example_id + "/") + + if __name__ == "__main__": unittest.main() From 8729770d6b8c6cd35ebb9935479c30a521f33621 Mon Sep 17 00:00:00 2001 From: Joshua Benning Date: Mon, 24 Mar 2025 21:19:04 +0100 Subject: [PATCH 42/45] api/server/services: Update exception handling to a CustomErrorResponse This should allow a response scheme that provides more information and sort of matches the way error feedback is specified --- .../services/aas_registry_server_service.py | 24 +++++-------- api/server/services/aas_service.py | 12 +++---- .../services/aasx_file_server_service.py | 14 +++----- .../submodel_registry_server_service.py | 35 ++++++------------- api/server/services/submodel_service.py | 24 ++++++------- api/server/utils/error_handling.py | 14 ++++++++ 6 files changed, 53 insertions(+), 70 deletions(-) create mode 100644 api/server/utils/error_handling.py diff --git a/api/server/services/aas_registry_server_service.py b/api/server/services/aas_registry_server_service.py index 97d126e..b992f43 100644 --- a/api/server/services/aas_registry_server_service.py +++ b/api/server/services/aas_registry_server_service.py @@ -2,9 +2,9 @@ from aas_core3 import jsonization from aas_core3.types import AssetAdministrationShell, ConceptDescription -from fastapi import HTTPException from basyx import ObjectStore +from server.utils.error_handling import CustomErrorResponse class AasRegistryServerService: @@ -13,9 +13,9 @@ def __init__(self, global_object_store: ObjectStore): def get_all_asset_administration_shell_descriptors(self) -> List[str]: all_descriptors = self.obj_store.get_identifiables_by_type(ConceptDescription) - print(all_descriptors.__dict__) - print(all_descriptors) - print("test") + # print(all_descriptors.__dict__) + # print(all_descriptors) + # print("test") aas_descriptors_store = ObjectStore() for descriptor in all_descriptors: reference_list = descriptor.is_case_of @@ -26,9 +26,7 @@ def get_all_asset_administration_shell_descriptors(self) -> List[str]: try: identifiable = self.obj_store.get_identifiable(reference_id) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) if isinstance(identifiable, AssetAdministrationShell): try: @@ -48,9 +46,7 @@ def post_asset_administration_shell_descriptor(self, json): try: self.obj_store.add(aas_descriptor) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "AAS Descriptor processed"} def put_asset_administration_shell_descriptor_by_id(self, json): @@ -60,16 +56,12 @@ def put_asset_administration_shell_descriptor_by_id(self, json): # update? self.obj_store.add(aas_descriptor) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "AASX package updated"} def delete_asset_administration_shell_descriptor_by_id(self, descriptor_id): try: self.obj_store.delete(descriptor_id) # should there be an exception if there is no aasx_package to delete? except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "AASX descriptor deleted"} diff --git a/api/server/services/aas_service.py b/api/server/services/aas_service.py index aebb922..1d57ab4 100644 --- a/api/server/services/aas_service.py +++ b/api/server/services/aas_service.py @@ -1,10 +1,10 @@ from typing import List, Type, Any, MutableMapping, Union from aas_core3 import jsonization -from aas_core3.types import AssetAdministrationShell, AssetInformation -from fastapi import HTTPException +from aas_core3.types import AssetAdministrationShell from basyx import ObjectStore +from server.utils.error_handling import CustomErrorResponse class AasService: @@ -19,7 +19,7 @@ def _get_all_shells(self) -> List[Type]: def _get_shell_by_id(self, aas_identifier) -> AssetAdministrationShell: shell = self.obj_store.get(aas_identifier) if shell is None or not isinstance(shell, AssetAdministrationShell): - raise HTTPException(status_code=404, detail="Submodel with id " + aas_identifier + " not found") + raise CustomErrorResponse(status_code=404, message="Submodel with id " + aas_identifier + " not found") return shell def _jsonable_shells(self, submodels: List[AssetAdministrationShell]) \ @@ -39,16 +39,14 @@ def add_shell_from_body(self, json): try: self.obj_store.add(shell) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "Shell processed"} def put_shell_by_id(self, aas_identifier, json): shell = self._get_shell_by_id(aas_identifier) new_shell = jsonization.asset_administration_shell_from_jsonable(json) if shell.id != new_shell.id: - raise HTTPException(403, "Shell with id " + aas_identifier + " does not match") + raise CustomErrorResponse(status_code=403, message="Shell with id " + aas_identifier + " does not match") self.obj_store.discard(shell) self.obj_store.add(new_shell) return jsonization.to_jsonable(new_shell) diff --git a/api/server/services/aasx_file_server_service.py b/api/server/services/aasx_file_server_service.py index e23c176..67ede61 100644 --- a/api/server/services/aasx_file_server_service.py +++ b/api/server/services/aasx_file_server_service.py @@ -2,9 +2,9 @@ from aas_core3.types import AssetAdministrationShell from aas_core3 import jsonization -from fastapi import HTTPException from basyx import ObjectStore +from server.utils.error_handling import CustomErrorResponse class AasxFileServerService: @@ -25,9 +25,7 @@ def post_aasx_package(self, json): try: self.obj_store.add(aasx_package) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "AASX package processed"} def put_aasx_by_package_id(self, json): @@ -37,16 +35,12 @@ def put_aasx_by_package_id(self, json): # update? self.obj_store.add(aasx_package) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "AASX package updated"} def delete_aasx_by_package_id(self, package_id): try: self.obj_store.delete(package_id) # should there be an exception if there is no aasx_package to delete? except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "AASX package deleted"} diff --git a/api/server/services/submodel_registry_server_service.py b/api/server/services/submodel_registry_server_service.py index 4c4bd1c..4b4197c 100644 --- a/api/server/services/submodel_registry_server_service.py +++ b/api/server/services/submodel_registry_server_service.py @@ -2,9 +2,9 @@ from aas_core3 import jsonization from aas_core3.types import Submodel, ConceptDescription -from fastapi import HTTPException from basyx import ObjectStore +from server.utils.error_handling import CustomErrorResponse class SubmodelRegistryServerService: @@ -29,8 +29,7 @@ def get_all_submodel_descriptors(self) -> List[str]: try: identifiable = self.obj_store.get_identifiable(reference_id.value) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log + # TODO: Handle error? Or is this intended? identifiable = [] if isinstance(identifiable, Submodel): try: @@ -44,9 +43,7 @@ def get_submodel_descriptor_by_id(self, descriptor_id) \ try: aas_descriptor = self.obj_store.get_identifiable(descriptor_id) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) assert isinstance(aas_descriptor, ConceptDescription) return jsonization.to_jsonable(aas_descriptor) @@ -61,20 +58,16 @@ def post_submodel_descriptor(self, json): try: self.obj_store.get_identifiable(reference_id.value) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail= "A referenced submodel of the concept description " + raise CustomErrorResponse(status_code=400, message="A referenced submodel of the concept description " "with the following id does not exist in the " - "object_store:" + str(e)) + "object_store", exception=e) try: self.obj_store.add(submodel_descriptor) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail="A referenced submodel of the concept description " + raise CustomErrorResponse(status_code=400, detail="A referenced submodel of the concept description " "with the following id does not exist in the " - "object_store:" + str(e)) + "object_store:", exception=e) return {"message": "Submodel descriptor processed"} def put_submodel_descriptor_by_id(self, json): @@ -88,27 +81,21 @@ def put_submodel_descriptor_by_id(self, json): try: self.obj_store.get_identifiable(reference_id.value) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail= "A referenced submodel of the concept description " + raise CustomErrorResponse(status_code=400, detail= "A referenced submodel of the concept description " "with the following id does not exist in the " - "object_store:" + str(e)) + "object_store:", exception=e) try: self.obj_store.delete(submodel_descriptor.id) # should there be an exception if there is no aasx_package to # update? self.obj_store.add(submodel_descriptor) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "AASX package updated"} def delete_submodel_descriptor_by_id(self, descriptor_id): try: self.obj_store.delete(descriptor_id) # should there be an exception if there is no aasx_package to delete? except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "Submodel descriptor deleted"} diff --git a/api/server/services/submodel_service.py b/api/server/services/submodel_service.py index d49244f..536ec0d 100644 --- a/api/server/services/submodel_service.py +++ b/api/server/services/submodel_service.py @@ -2,9 +2,9 @@ from aas_core3 import jsonization from aas_core3.types import Submodel, SubmodelElement, AssetAdministrationShell -from fastapi import HTTPException from basyx import ObjectStore +from server.utils.error_handling import CustomErrorResponse class SubmodelService: @@ -20,7 +20,7 @@ def _get_all_submodel_references_by_shell(self, aas_identifier: str) -> List[Typ if isinstance(shell, AssetAdministrationShell): return shell.submodels # FIXME: Typing issues, should this be (safe) casted? else: - raise HTTPException(status_code=404, detail="AAS " + aas_identifier + " not found") + raise CustomErrorResponse(status_code=404, message="AAS " + aas_identifier + " not found") def _jsonable_submodels(self, submodels: List[Submodel]) \ -> List[Union[bool, int, float, str, List[Any], MutableMapping[str, Any]]]: @@ -29,7 +29,7 @@ def _jsonable_submodels(self, submodels: List[Submodel]) \ def _get_submodel_by_id(self, submodel_id): submodel = self.obj_store.get(submodel_id) if submodel is None or not isinstance(submodel, Submodel): - raise HTTPException(status_code=404, detail="Submodel with id " + submodel_id + " not found") + raise CustomErrorResponse(status_code=404, message="Submodel with id " + submodel_id + " not found") return submodel def _get_submodel_element_by_id_short(self, submodel_id, element_id_short): @@ -50,9 +50,7 @@ def add_submodel_from_body(self, json): try: self.obj_store.add(submodel) except KeyError as e: - # TODO: Provide a stacktrace - # Wenn anders in Spezifikation, Stacktrace in server log - raise HTTPException(status_code=400, detail=str(e)) + raise CustomErrorResponse(status_code=400, exception=e) return {"message": "Submodel processed"} def get_submodel_jsonable_by_id(self, submodel_id: str): @@ -63,7 +61,7 @@ def update_submodel_by_id(self, submodel_id: str, json): submodel = self._get_submodel_by_id(submodel_id) new_submodel = jsonization.submodel_from_jsonable(json) if submodel.id != new_submodel.id: - raise HTTPException(403, "Submodel with id " + submodel_id + " does not match") + raise CustomErrorResponse(status_code=403, message="Submodel with id " + submodel_id + " does not match") self.obj_store.discard(submodel) self.obj_store.add(new_submodel) return jsonization.to_jsonable(new_submodel) @@ -95,7 +93,7 @@ def update_submodel_elements(self, submodel_id, json): def get_submodel_element(self, submodel_id, element_short_id): element = self._get_submodel_element_by_id_short(submodel_id, element_short_id) if element is None: - raise HTTPException(status_code=404, detail="Submodel element with id " + element_short_id + " not found.") + raise CustomErrorResponse(status_code=404, message="Submodel element with id " + element_short_id + " not found.") return jsonization.to_jsonable(element) def post_submodel_element(self, submodel_id, body): @@ -106,16 +104,16 @@ def post_submodel_element(self, submodel_id, body): submodel.submodel_elements.append(submodel_element) return jsonization.to_jsonable(submodel_element) else: - raise HTTPException(status_code=400, - detail="Submodel element with id " + submodel_element.id_short + " already exists") + raise CustomErrorResponse(status_code=400, + message="Submodel element with id " + submodel_element.id_short + " already exists") def put_submodel_element(self, submodel_id, body): submodel = self._get_submodel_by_id(submodel_id) submodel_element = jsonization.submodel_element_from_jsonable(body) existing_submodel_element = self._get_submodel_element_by_id_short(submodel_id, submodel_element.id_short) if existing_submodel_element is None: - raise HTTPException(status_code=404, - detail="Submodel element with id " + submodel_element.id_short + " does not exist") + raise CustomErrorResponse(status_code=404, + message="Submodel element with id " + submodel_element.id_short + " does not exist") else: submodel.submodel_elements.remove(existing_submodel_element) submodel.submodel_elements.append(submodel_element) @@ -125,7 +123,7 @@ def delete_submodel_element(self, submodel_id, id_short): submodel = self._get_submodel_by_id(submodel_id) existing_submodel_element = self._get_submodel_element_by_id_short(submodel_id, id_short) if existing_submodel_element is None: - raise HTTPException(status_code=404, detail="Submodel element with id " + id_short + " does not exist") + raise CustomErrorResponse(status_code=404, message="Submodel element with id " + id_short + " does not exist") else: submodel.submodel_elements.remove(existing_submodel_element) return jsonization.to_jsonable(existing_submodel_element) diff --git a/api/server/utils/error_handling.py b/api/server/utils/error_handling.py new file mode 100644 index 0000000..10a08eb --- /dev/null +++ b/api/server/utils/error_handling.py @@ -0,0 +1,14 @@ +import traceback +from fastapi import HTTPException + +class CustomErrorResponse(HTTPException): + def __init__(self, exception: Exception = Exception("No exception provided"), + message: str = "No additional information was provided.", status_code: int = 400): + super().__init__( + status_code=status_code, + detail={ + "message": message, + "error": str(exception), + "stacktrace": traceback.format_exc(), + }, + ) From 125f89f251e04369e573dfab27a04397f8472141 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Mon, 16 Jun 2025 09:11:19 +0200 Subject: [PATCH 43/45] api/pyproject.toml: Add empty line at end of file --- api/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 987d077..c9e3f0f 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -23,4 +23,4 @@ dev = [ "codeblocks", "coverage", "httpx" -] \ No newline at end of file +] From 15e6377ee50e6d98c4c306d7695a79e95e71896a Mon Sep 17 00:00:00 2001 From: s-heppner Date: Mon, 16 Jun 2025 09:12:00 +0200 Subject: [PATCH 44/45] server.routes.aasx_file_server: Add empty line at end of file --- api/server/routes/aasx_file_server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/server/routes/aasx_file_server.py b/api/server/routes/aasx_file_server.py index cdc7bcc..703f4bb 100644 --- a/api/server/routes/aasx_file_server.py +++ b/api/server/routes/aasx_file_server.py @@ -36,4 +36,3 @@ async def put_assx_package(request: Request) -> Any: @self.router.delete("/{aasx_package_id}") async def delete_aasx_package_by_id(aasx_package_id: str) -> Any: return self.service.delete_aasx_by_package_id(aasx_package_id) - \ No newline at end of file From 4c32602d9e3b9200bacfda843acc8e160fcbb3d9 Mon Sep 17 00:00:00 2001 From: s-heppner Date: Mon, 16 Jun 2025 09:20:45 +0200 Subject: [PATCH 45/45] api.test.examples: Add missing lines at end of files --- api/test/examples/aas/aas.json | 2 +- api/test/examples/aas/aas_modified.json | 2 +- api/test/examples/aas/asset_information.json | 2 +- api/test/examples/aas/asset_information_modified.json | 2 +- api/test/examples/aas/asset_information_no_thumbnail.json | 2 +- api/test/examples/aas/thumbnail.json | 2 +- api/test/examples/aas/thumbnail_modified.json | 2 +- api/test/examples/aasx/aasx.json | 2 +- api/test/examples/submodel/submodel.json | 2 +- api/test/examples/submodel/submodel_element.json | 2 +- api/test/examples/submodel/submodel_element_new.json | 2 +- api/test/examples/submodel/submodel_modified.json | 2 +- api/test/examples/submodel/submodel_with_new_element.json | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/api/test/examples/aas/aas.json b/api/test/examples/aas/aas.json index 67ed2f6..f2f276c 100644 --- a/api/test/examples/aas/aas.json +++ b/api/test/examples/aas/aas.json @@ -7,4 +7,4 @@ } }, "modelType": "AssetAdministrationShell" -} \ No newline at end of file +} diff --git a/api/test/examples/aas/aas_modified.json b/api/test/examples/aas/aas_modified.json index f5baf9d..f5a1d36 100644 --- a/api/test/examples/aas/aas_modified.json +++ b/api/test/examples/aas/aas_modified.json @@ -7,4 +7,4 @@ } }, "modelType": "AssetAdministrationShell" -} \ No newline at end of file +} diff --git a/api/test/examples/aas/asset_information.json b/api/test/examples/aas/asset_information.json index 6075246..00120dd 100644 --- a/api/test/examples/aas/asset_information.json +++ b/api/test/examples/aas/asset_information.json @@ -3,4 +3,4 @@ "defaultThumbnail": { "path": "blub" } -} \ No newline at end of file +} diff --git a/api/test/examples/aas/asset_information_modified.json b/api/test/examples/aas/asset_information_modified.json index 12574f8..d27838e 100644 --- a/api/test/examples/aas/asset_information_modified.json +++ b/api/test/examples/aas/asset_information_modified.json @@ -3,4 +3,4 @@ "defaultThumbnail": { "path": "blub" } -} \ No newline at end of file +} diff --git a/api/test/examples/aas/asset_information_no_thumbnail.json b/api/test/examples/aas/asset_information_no_thumbnail.json index 25838a9..4ac0bf9 100644 --- a/api/test/examples/aas/asset_information_no_thumbnail.json +++ b/api/test/examples/aas/asset_information_no_thumbnail.json @@ -1,3 +1,3 @@ { "assetKind": "Type" -} \ No newline at end of file +} diff --git a/api/test/examples/aas/thumbnail.json b/api/test/examples/aas/thumbnail.json index edc4f93..f1be4d5 100644 --- a/api/test/examples/aas/thumbnail.json +++ b/api/test/examples/aas/thumbnail.json @@ -1,3 +1,3 @@ { "path": "blub" -} \ No newline at end of file +} diff --git a/api/test/examples/aas/thumbnail_modified.json b/api/test/examples/aas/thumbnail_modified.json index 7a615dc..1621570 100644 --- a/api/test/examples/aas/thumbnail_modified.json +++ b/api/test/examples/aas/thumbnail_modified.json @@ -1,3 +1,3 @@ { "path": "bluuub" -} \ No newline at end of file +} diff --git a/api/test/examples/aasx/aasx.json b/api/test/examples/aasx/aasx.json index 8e38447..8ff2c4c 100644 --- a/api/test/examples/aasx/aasx.json +++ b/api/test/examples/aasx/aasx.json @@ -4,4 +4,4 @@ "assetKind":"Type" }, "modelType":"AssetAdministrationShell" -} \ No newline at end of file +} diff --git a/api/test/examples/submodel/submodel.json b/api/test/examples/submodel/submodel.json index b52c719..d308fa2 100644 --- a/api/test/examples/submodel/submodel.json +++ b/api/test/examples/submodel/submodel.json @@ -34,4 +34,4 @@ } ], "modelType": "Submodel" -} \ No newline at end of file +} diff --git a/api/test/examples/submodel/submodel_element.json b/api/test/examples/submodel/submodel_element.json index 48ef945..2fabdfa 100644 --- a/api/test/examples/submodel/submodel_element.json +++ b/api/test/examples/submodel/submodel_element.json @@ -3,4 +3,4 @@ "valueType": "xs:int", "value": "8419", "modelType": "Property" -} \ No newline at end of file +} diff --git a/api/test/examples/submodel/submodel_element_new.json b/api/test/examples/submodel/submodel_element_new.json index 48ef945..2fabdfa 100644 --- a/api/test/examples/submodel/submodel_element_new.json +++ b/api/test/examples/submodel/submodel_element_new.json @@ -3,4 +3,4 @@ "valueType": "xs:int", "value": "8419", "modelType": "Property" -} \ No newline at end of file +} diff --git a/api/test/examples/submodel/submodel_modified.json b/api/test/examples/submodel/submodel_modified.json index 6d2cf71..80233be 100644 --- a/api/test/examples/submodel/submodel_modified.json +++ b/api/test/examples/submodel/submodel_modified.json @@ -34,4 +34,4 @@ } ], "modelType": "Submodel" -} \ No newline at end of file +} diff --git a/api/test/examples/submodel/submodel_with_new_element.json b/api/test/examples/submodel/submodel_with_new_element.json index be7a61c..4a2686b 100644 --- a/api/test/examples/submodel/submodel_with_new_element.json +++ b/api/test/examples/submodel/submodel_with_new_element.json @@ -40,4 +40,4 @@ } ], "modelType": "Submodel" -} \ No newline at end of file +}