From 034136117d2626bdaebf9c0d28ae1aa6f022ddd2 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Tue, 14 Oct 2025 13:47:14 +0200 Subject: [PATCH 1/6] update AAS_Repository to V3.1 --- server/README.md | 4 +- server/app/interfaces/base.py | 57 +++++++++++++++++++++++++++-- server/app/interfaces/repository.py | 44 ++++++++++++++++++---- 3 files changed, 93 insertions(+), 12 deletions(-) diff --git a/server/README.md b/server/README.md index 979771cf..12be28b4 100644 --- a/server/README.md +++ b/server/README.md @@ -103,9 +103,9 @@ The server can also be run directly on the host system without Docker, NGINX and $ pip install ./app ``` -2. Run the server by executing the main function in [`./app/interfaces/repository.py`](./app/interfaces/repository.py) from within the current folder. +2. Run the server by executing the main function in [`./app/interfaces/repository.py`](./app/interfaces/repository.py) from within the `app` folder. ```bash - $ python -m app.interfaces.repository + $ python -m interfaces.repository ``` The server can be accessed at http://localhost:8080/api/v3.0/ from your host system. diff --git a/server/app/interfaces/base.py b/server/app/interfaces/base.py index 686bb92c..c1108169 100644 --- a/server/app/interfaces/base.py +++ b/server/app/interfaces/base.py @@ -30,6 +30,58 @@ T = TypeVar("T") +class ServiceSpecificationProfileEnum(str, enum.Enum): + """ + Enumeration of all standardized Service Specification Profiles + from the AAS Part 2 API Specification (IDTA-01002-3-1). + Each profile is uniquely identified by its semantic URI. + """ + + # --- Asset Administration Shell (AAS) --- + AAS_FULL = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellServiceSpecification/SSP-001" + AAS_READ = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellServiceSpecification/SSP-002" + + # --- Submodel --- + SUBMODEL_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-001" + SUBMODEL_VALUE = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-002" + SUBMODEL_READ = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-003" + + # --- AASX File Server --- + AASX_FILESERVER_FULL = "https://admin-shell.io/aas/API/3/1/AasxFileServerServiceSpecification/SSP-001" + + # --- AAS Registry --- + AAS_REGISTRY_FULL = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-001" + AAS_REGISTRY_READ = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-002" + AAS_REGISTRY_BULK = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-003" + + # --- Submodel Registry --- + SUBMODEL_REGISTRY_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-001" + SUBMODEL_REGISTRY_READ = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-002" + SUBMODEL_REGISTRY_BULK = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-003" + + # --- AAS Repository --- + AAS_REPOSITORY_FULL = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-001" + AAS_REPOSITORY_READ = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-002" + AAS_REPOSITORY_BULK = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-003" + + # --- Submodel Repository --- + SUBMODEL_REPOSITORY_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-001" + SUBMODEL_REPOSITORY_READ = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-002" + SUBMODEL_REPOSITORY_BULK = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-003" + + # --- Concept Description Repository --- + CONCEPT_DESCRIPTION_REPOSITORY_FULL = "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-001" + CONCEPT_DESCRIPTION_REPOSITORY_READ = "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-002" + CONCEPT_DESCRIPTION_REPOSITORY_BULK = "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-003" + + # --- Discovery --- + DISCOVERY_FULL = "https://admin-shell.io/aas/API/3/1/DiscoveryServiceSpecification/SSP-001" + DISCOVERY_READ = "https://admin-shell.io/aas/API/3/1/DiscoveryServiceSpecification/SSP-002" + +#TODO: Maybe remove this in spite of spec? Too complicated structure +class ServiceDescription: + def __init__(self, profiles: List[ServiceSpecificationProfileEnum]): + self.profiles: List[ServiceSpecificationProfileEnum] = profiles @enum.unique class MessageType(enum.Enum): @@ -84,7 +136,7 @@ def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: - if cursor is None: + if cursor is None or (isinstance(obj, list) and not obj): data = obj else: data = { @@ -104,7 +156,7 @@ def __init__(self, *args, content_type="application/xml", **kwargs): def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: root_elem = etree.Element("response", nsmap=XML_NS_MAP) - if cursor is not None: + if cursor is not None or not (isinstance(obj, list) and not obj): root_elem.set("cursor", str(cursor)) if isinstance(obj, Result): result_elem = self.result_to_xml(obj, **XML_NS_MAP) @@ -163,7 +215,6 @@ class ResultToJsonEncoder(AASToJsonEncoder): @classmethod def _result_to_json(cls, result: Result) -> Dict[str, object]: return { - "success": result.success, "messages": result.messages } diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index 03a3974c..9336d1a5 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -49,8 +49,13 @@ from basyx.aas import model from basyx.aas.adapter import aasx from util.converters import IdentifierToBase64URLConverter, IdShortPathConverter, base64url_decode -from .base import ObjectStoreWSGIApp, APIResponse, is_stripped_request, HTTPApiDecoder, T +from .base import (ObjectStoreWSGIApp, APIResponse, is_stripped_request, HTTPApiDecoder, T, + ServiceSpecificationProfileEnum, ServiceDescription) +SUPPORTED_PROFILES: ServiceDescription = ServiceDescription([ + ServiceSpecificationProfileEnum.AAS_REPOSITORY_FULL, + ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_FULL, +]) class WSGIApp(ObjectStoreWSGIApp): def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.AbstractSupplementaryFileContainer, @@ -60,7 +65,7 @@ def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.Abs self.url_map = werkzeug.routing.Map([ Submount(base_path, [ Rule("/serialization", methods=["GET"], endpoint=self.not_implemented), - Rule("/description", methods=["GET"], endpoint=self.not_implemented), + Rule("/description", methods=["GET"], endpoint=self.get_description), Rule("/shells", methods=["GET"], endpoint=self.get_aas_all), Rule("/shells", methods=["POST"], endpoint=self.post_aas), Submount("/shells", [ @@ -200,9 +205,15 @@ def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._ return identifiable def _get_all_obj_of_type(self, type_: Type[model.provider._IT]) -> Iterator[model.provider._IT]: - for obj in self.object_store: - if isinstance(obj, type_): - yield obj + matching_identifiables = [] + for identifiable in self.object_store: + if isinstance(identifiable, type_): + matching_identifiables.append(identifiable) + + sorted_identifiables = sorted(matching_identifiables, key=lambda identifiable: identifiable.id) + + for identifiable in sorted_identifiables: + yield identifiable def _resolve_reference(self, reference: model.ModelReference[model.base._RT]) -> model.base._RT: try: @@ -341,6 +352,13 @@ def _get_concept_description(self, url_args): def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: raise werkzeug.exceptions.NotImplemented("This route is not implemented!") + def get_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: + profiles = [] + for profile in SUPPORTED_PROFILES.profiles: + profiles.append(profile.value) + description = {"profiles": profiles} + return response_t(description) + # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: aashells, cursor = self._get_shells(request) @@ -405,13 +423,17 @@ def get_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Ty return response_t(list(submodel_refs), cursor=cursor) def post_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Type[APIResponse], - **_kwargs) -> Response: + map_adapter: MapAdapter, **_kwargs) -> Response: aas = self._get_shell(url_args) sm_ref = HTTPApiDecoder.request_body(request, model.ModelReference, False) if sm_ref in aas.submodel: raise Conflict(f"{sm_ref!r} already exists!") aas.submodel.add(sm_ref) - return response_t(sm_ref, status=201) + created_resource_url = map_adapter.build(self.delete_aas_submodel_refs_specific, { + "aas_id": aas.id, + "submodel_id": sm_ref.key[0].value + }, force_external=True) + return response_t(sm_ref, status=201, headers={"Location": created_resource_url}) def delete_aas_submodel_refs_specific(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: @@ -476,6 +498,8 @@ def post_submodel(self, request: Request, url_args: Dict, response_t: Type[APIRe def get_submodel_all_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: + if "level" in request.args: + raise BadRequest(f"level cannot be used when retrieving metadata!") submodels, cursor = self._get_submodels(request) return response_t(list(submodels), cursor=cursor, stripped=True) @@ -498,6 +522,8 @@ def get_submodel(self, request: Request, url_args: Dict, response_t: Type[APIRes def get_submodels_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: + if "level" in request.args: + raise BadRequest(f"level cannot be used when retrieving metadata!") submodel = self._get_submodel(url_args) return response_t(submodel, stripped=True) @@ -519,6 +545,8 @@ def get_submodel_submodel_elements(self, request: Request, url_args: Dict, respo def get_submodel_submodel_elements_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: + if "level" in request.args: + raise BadRequest(f"level cannot be used when retrieving metadata!") submodel_elements, cursor = self._get_submodel_submodel_elements(request, url_args) return response_t(list(submodel_elements), cursor=cursor, stripped=True) @@ -537,6 +565,8 @@ def get_submodel_submodel_elements_id_short_path(self, request: Request, url_arg def get_submodel_submodel_elements_id_short_path_metadata(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: + if "level" in request.args: + raise BadRequest(f"level cannot be used when retrieving metadata!") submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) if isinstance(submodel_element, model.Capability) or isinstance(submodel_element, model.Operation): raise BadRequest(f"{submodel_element.id_short} does not allow the content modifier metadata!") From 31a17cc97ce2955db1e19ca73ea1653fcba09412 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Tue, 14 Oct 2025 13:56:22 +0200 Subject: [PATCH 2/6] fix pycodestyle errors --- server/app/interfaces/base.py | 32 ++++++++++++++++++++--------- server/app/interfaces/repository.py | 1 + 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/server/app/interfaces/base.py b/server/app/interfaces/base.py index c1108169..2abd38ba 100644 --- a/server/app/interfaces/base.py +++ b/server/app/interfaces/base.py @@ -30,6 +30,7 @@ T = TypeVar("T") + class ServiceSpecificationProfileEnum(str, enum.Enum): """ Enumeration of all standardized Service Specification Profiles @@ -50,9 +51,12 @@ class ServiceSpecificationProfileEnum(str, enum.Enum): AASX_FILESERVER_FULL = "https://admin-shell.io/aas/API/3/1/AasxFileServerServiceSpecification/SSP-001" # --- AAS Registry --- - AAS_REGISTRY_FULL = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-001" - AAS_REGISTRY_READ = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-002" - AAS_REGISTRY_BULK = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-003" + AAS_REGISTRY_FULL = \ + "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-001" + AAS_REGISTRY_READ = \ + "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-002" + AAS_REGISTRY_BULK = \ + "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-003" # --- Submodel Registry --- SUBMODEL_REGISTRY_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-001" @@ -60,9 +64,12 @@ class ServiceSpecificationProfileEnum(str, enum.Enum): SUBMODEL_REGISTRY_BULK = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-003" # --- AAS Repository --- - AAS_REPOSITORY_FULL = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-001" - AAS_REPOSITORY_READ = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-002" - AAS_REPOSITORY_BULK = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-003" + AAS_REPOSITORY_FULL = \ + "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-001" + AAS_REPOSITORY_READ = \ + "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-002" + AAS_REPOSITORY_BULK = \ + "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-003" # --- Submodel Repository --- SUBMODEL_REPOSITORY_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-001" @@ -70,19 +77,24 @@ class ServiceSpecificationProfileEnum(str, enum.Enum): SUBMODEL_REPOSITORY_BULK = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-003" # --- Concept Description Repository --- - CONCEPT_DESCRIPTION_REPOSITORY_FULL = "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-001" - CONCEPT_DESCRIPTION_REPOSITORY_READ = "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-002" - CONCEPT_DESCRIPTION_REPOSITORY_BULK = "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-003" + CONCEPT_DESCRIPTION_REPOSITORY_FULL = \ + "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-001" + CONCEPT_DESCRIPTION_REPOSITORY_READ = \ + "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-002" + CONCEPT_DESCRIPTION_REPOSITORY_BULK = \ + "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-003" # --- Discovery --- DISCOVERY_FULL = "https://admin-shell.io/aas/API/3/1/DiscoveryServiceSpecification/SSP-001" DISCOVERY_READ = "https://admin-shell.io/aas/API/3/1/DiscoveryServiceSpecification/SSP-002" -#TODO: Maybe remove this in spite of spec? Too complicated structure + +# TODO: Maybe remove this in spite of spec? Too complicated structure class ServiceDescription: def __init__(self, profiles: List[ServiceSpecificationProfileEnum]): self.profiles: List[ServiceSpecificationProfileEnum] = profiles + @enum.unique class MessageType(enum.Enum): UNDEFINED = enum.auto() diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index 9336d1a5..6947211c 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -57,6 +57,7 @@ ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_FULL, ]) + class WSGIApp(ObjectStoreWSGIApp): def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.AbstractSupplementaryFileContainer, base_path: str = "/api/v3.0"): From 9aa3aa65ee0cd250cc95d909542b9a138c227b42 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Tue, 14 Oct 2025 14:04:46 +0200 Subject: [PATCH 3/6] update docstring --- server/app/interfaces/repository.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index 6947211c..f3e83e17 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -15,10 +15,9 @@ 3. Route `/shells/{aasIdentifier}/asset-information/thumbnail`: Not implemented because the specification lacks clarity. -4. Serialization and Description Routes: +4. Serialization Route: - `/serialization` - - `/description` - These routes are not implemented at this time. + This route is not implemented at this time. 5. Value, Path, and PATCH Routes: - All `/…/value$`, `/…/path$`, and `PATCH` routes are currently not implemented. From 01f04a40fb7a8dd83808aafd8101808646653ef0 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Tue, 14 Oct 2025 14:31:09 +0200 Subject: [PATCH 4/6] add deterministic pagination to submodel_refs --- server/app/interfaces/repository.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index f3e83e17..b80bca59 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -3,7 +3,7 @@ # This program and the accompanying materials are made available under the terms of the MIT License, available in # the LICENSE file of this project. # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: MITd """ This module implements the "Specification of the Asset Administration Shell Part 2 Application Programming Interfaces". However, several features and routes are currently not supported: @@ -419,7 +419,8 @@ def get_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Ty **_kwargs) -> Response: aas = self._get_shell(url_args) submodel_refs: Iterator[model.ModelReference[model.Submodel]] - submodel_refs, cursor = self._get_slice(request, aas.submodel) + sorted_submodel_refs = sorted(aas.submodel, key=lambda ref: ref.key[0].value) + submodel_refs, cursor = self._get_slice(request, sorted_submodel_refs) return response_t(list(submodel_refs), cursor=cursor) def post_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Type[APIResponse], From ca117980464c8ffd2b51678ea403d917ba8a0d46 Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Wed, 22 Oct 2025 20:18:46 +0200 Subject: [PATCH 5/6] update README, API is version 3.1.1 --- README.md | 4 ++-- server/README.md | 12 ++++++------ server/app/interfaces/repository.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 82e56055..2051bf8e 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ These are the currently implemented specifications: | Specification | Version | |---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Part 1: Metamodel | [v3.0.1 (01001-3-0-1)](https://industrialdigitaltwin.org/wp-content/uploads/2024/06/IDTA-01001-3-0-1_SpecificationAssetAdministrationShell_Part1_Metamodel.pdf) | +| Part 1: Metamodel | [v3.0.1 (01001-3-0-1)](https://industrialdigitaltwin.org/wp-content/uploads/2024/06/IDTA-01001-3-0-1_SpecificationAssetAdministrationShell_Part1_Metamodel.pdf) | | Schemata (JSONSchema, XSD) | [v3.0.8 (IDTA-01001-3-0-1_schemasV3.0.8)](https://github.com/admin-shell-io/aas-specs/releases/tag/IDTA-01001-3-0-1_schemasV3.0.8) | -| Part 2: API | [v3.0 (01002-3-0)](https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2023/06/IDTA-01002-3-0_SpecificationAssetAdministrationShell_Part2_API_.pdf) | +| Part 2: API | [v3.1.1 (01002-3-1-1)](https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2025/08/IDTA-01002-3-1-1_AAS-Specification_Part2_API.pdf) | | Part 3a: Data Specification IEC 61360 | [v3.0 (01003-a-3-0)](https://industrialdigitaltwin.org/wp-content/uploads/2023/04/IDTA-01003-a-3-0_SpecificationAssetAdministrationShell_Part3a_DataSpecification_IEC61360.pdf) | | Part 5: Package File Format (AASX) | [v3.0 (01005-3-0)](https://industrialdigitaltwin.org/wp-content/uploads/2023/04/IDTA-01005-3-0_SpecificationAssetAdministrationShell_Part5_AASXPackageFileFormat.pdf) | diff --git a/server/README.md b/server/README.md index 12be28b4..58107a91 100644 --- a/server/README.md +++ b/server/README.md @@ -40,7 +40,7 @@ To expose it on the host on port 8080, use the option `-p 8080:80` when running The container can be configured via environment variables: - `API_BASE_PATH` determines the base path under which all other API paths are made available. - Default: `/api/v3.0` + Default: `/api/v3.1` - `STORAGE_TYPE` can be one of `LOCAL_FILE_READ_ONLY` or `LOCAL_FILE_BACKEND`: - When set to `LOCAL_FILE_READ_ONLY` (the default), the server will read and serve AASX, JSON, XML files from the storage directory. The files are not modified, all changes done via the API are only stored in memory. @@ -60,7 +60,7 @@ Since Windows uses backslashes instead of forward slashes in paths, you'll have > docker run -p 8080:80 -v .\storage:/storage basyx-python-server ``` -Per default, the server will use the `LOCAL_FILE_READ_ONLY` storage type and serve the API under `/api/v3.0` and read files from `/storage`. If you want to change this, you can do so like this: +Per default, the server will use the `LOCAL_FILE_READ_ONLY` storage type and serve the API under `/api/v3.1` and read files from `/storage`. If you want to change this, you can do so like this: ``` $ docker run -p 8080:80 -v ./storage2:/storage2 -e API_BASE_PATH=/api/v3.1 -e STORAGE_TYPE=LOCAL_FILE_BACKEND -e STORAGE_PATH=/storage2 basyx-python-server ``` @@ -85,7 +85,7 @@ services: - ./storage:/storage ``` -Here files are read from `/storage` and the server can be accessed at http://localhost:8080/api/v3.0/ from your host system. +Here files are read from `/storage` and the server can be accessed at http://localhost:8080/api/v3.1/ from your host system. To get a different setup this compose.yaml file can be adapted and expanded. Note that the `Dockerfile` has to be specified explicitly, as the build context must be set to the parent directory of `/server` to allow access to the local `/sdk`. @@ -108,7 +108,7 @@ The server can also be run directly on the host system without Docker, NGINX and $ python -m interfaces.repository ``` -The server can be accessed at http://localhost:8080/api/v3.0/ from your host system. +The server can be accessed at http://localhost:8080/api/v3.1/ from your host system. ## Acknowledgments @@ -117,8 +117,8 @@ This Dockerfile is inspired by the [tiangolo/uwsgi-nginx-docker][10] repository. [1]: https://github.com/eclipse-basyx/basyx-python-sdk/pull/238 [2]: https://basyx-python-sdk.readthedocs.io/en/latest/backend/local_file.html [3]: https://github.com/eclipse-basyx/basyx-python-sdk -[4]: https://app.swaggerhub.com/apis/Plattform_i40/AssetAdministrationShellRepositoryServiceSpecification/V3.0.1_SSP-001 -[5]: https://app.swaggerhub.com/apis/Plattform_i40/SubmodelRepositoryServiceSpecification/V3.0.1_SSP-001 +[4]: https://app.swaggerhub.com/apis/Plattform_i40/AssetAdministrationShellRepositoryServiceSpecification/V3.1.1_SSP-001 +[5]: https://app.swaggerhub.com/apis/Plattform_i40/SubmodelRepositoryServiceSpecification/V3.1.1_SSP-001 [6]: https://industrialdigitaltwin.org/content-hub/aasspecifications/idta_01002-3-0_application_programming_interfaces [7]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/aasx.html#adapter-aasx [8]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/json.html diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index b80bca59..26e695c0 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -59,7 +59,7 @@ class WSGIApp(ObjectStoreWSGIApp): def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.AbstractSupplementaryFileContainer, - base_path: str = "/api/v3.0"): + base_path: str = "/api/v3.1"): self.object_store: model.AbstractObjectStore = object_store self.file_store: aasx.AbstractSupplementaryFileContainer = file_store self.url_map = werkzeug.routing.Map([ From 54473f0792254823334023f3d002f7ad079fe8db Mon Sep 17 00:00:00 2001 From: Sercan Sahin Date: Mon, 3 Nov 2025 13:23:40 +0100 Subject: [PATCH 6/6] remove cursor when end of result set reached --- server/app/interfaces/base.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/app/interfaces/base.py b/server/app/interfaces/base.py index 2abd38ba..b861990f 100644 --- a/server/app/interfaces/base.py +++ b/server/app/interfaces/base.py @@ -148,7 +148,7 @@ def __init__(self, *args, content_type="application/json", **kwargs): super().__init__(*args, **kwargs, content_type=content_type) def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str: - if cursor is None or (isinstance(obj, list) and not obj): + if cursor is None: data = obj else: data = { @@ -273,8 +273,11 @@ def _get_slice(cls, request: Request, iterator: Iterable[T]) -> Tuple[Iterator[T raise BadRequest("Limit can not be negative, cursor must be positive!") start_index = cursor end_index = cursor + limit - paginated_slice = itertools.islice(iterator, start_index, end_index) - return paginated_slice, end_index + items = list(itertools.islice(iterator, start_index, end_index + 1)) + has_more = len(items) > limit + paginated_slice = iter(items[:limit]) + next_cursor = cursor + limit if has_more else None + return paginated_slice, next_cursor def handle_request(self, request: Request): map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ)