From e81b501926de6550e48773881beaa4f2549e0539 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 12 May 2026 19:18:08 +0200 Subject: [PATCH 01/13] fix: align ServiceSpecificationProfileEnum with IDTA-01002 v3.1.2 spec - Fix SUBMODEL_READ/SUBMODEL_VALUE names (were swapped: SSP-002=Read, SSP-003=Value) - Rename AAS_REPOSITORY_BULK -> AAS_REPOSITORY_QUERY (SSP-003 is Query, no Bulk exists) - Rename SUBMODEL_REPOSITORY_BULK -> SUBMODEL_REPOSITORY_TEMPLATE (SSP-003 is Template) - Rename CONCEPT_DESCRIPTION_REPOSITORY_READ -> CONCEPT_DESCRIPTION_REPOSITORY_QUERY (SSP-002) - Remove CONCEPT_DESCRIPTION_REPOSITORY_BULK (SSP-003 does not exist in spec) - Add AAS_REGISTRY_QUERY (SSP-004), AAS_REGISTRY_MINIMAL_READ (SSP-005) - Add SUBMODEL_REGISTRY_QUERY (SSP-004) - Add SUBMODEL_REPOSITORY_TEMPLATE_READ (SSP-004), SUBMODEL_REPOSITORY_QUERY (SSP-005) --- server/app/model/service_specification.py | 27 ++++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/server/app/model/service_specification.py b/server/app/model/service_specification.py index 00b4a5da..5181901a 100644 --- a/server/app/model/service_specification.py +++ b/server/app/model/service_specification.py @@ -5,8 +5,11 @@ class ServiceSpecificationProfileEnum(str, enum.Enum): """ Enumeration of all standardized Service Specification Profiles - from the AAS Part 2 API Specification (IDTA-01002-3-1). + from the AAS Part 2 API Specification (IDTA-01002-3-1-2). Each profile is uniquely identified by its semantic URI. + + Reference: https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.1.2/ + http-rest-api/service-specifications-and-profiles.html """ # --- Asset Administration Shell (AAS) --- @@ -15,8 +18,8 @@ class ServiceSpecificationProfileEnum(str, enum.Enum): # --- 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" + SUBMODEL_READ = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-002" + SUBMODEL_VALUE = "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" @@ -28,32 +31,40 @@ class ServiceSpecificationProfileEnum(str, enum.Enum): "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_QUERY = \ + "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-004" + AAS_REGISTRY_MINIMAL_READ = \ + "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-005" # --- 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" + SUBMODEL_REGISTRY_QUERY = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-004" # --- 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 = \ + AAS_REPOSITORY_QUERY = \ "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" + SUBMODEL_REPOSITORY_TEMPLATE = \ + "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-003" + SUBMODEL_REPOSITORY_TEMPLATE_READ = \ + "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-004" + SUBMODEL_REPOSITORY_QUERY = \ + "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-005" # --- Concept Description Repository --- CONCEPT_DESCRIPTION_REPOSITORY_FULL = \ "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-001" - CONCEPT_DESCRIPTION_REPOSITORY_READ = \ + CONCEPT_DESCRIPTION_REPOSITORY_QUERY = \ "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" From 63ed755fba0c455ccf448af9bb9f0dd678e0fb43 Mon Sep 17 00:00:00 2001 From: Igor Garmaev <56840636+zrgt@users.noreply.github.com> Date: Tue, 12 May 2026 17:35:33 +0200 Subject: [PATCH 02/13] fix: pass paging_metadata kwarg correctly in get_aas_submodel_refs and get_concept_description_all Closes #539 Both handlers called response_t(..., cursor=cursor) but APIResponse.__init__ accepts paging_metadata, not cursor. _get_slice() already returns Optional[PagingMetadata] as its second value, so the variable just needs to be passed under the correct keyword argument name. --- server/app/interfaces/repository.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index 12d11209..5e038ee5 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -589,7 +589,7 @@ def get_aas_submodel_refs( submodel_refs: Iterator[model.ModelReference[model.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) + return response_t(list(submodel_refs), paging_metadata=cursor) def post_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: Type[APIResponse], map_adapter: MapAdapter, **_kwargs) -> Response: @@ -948,7 +948,7 @@ def get_concept_description_all( ) -> Response: concept_descriptions: Iterator[model.ConceptDescription] = self._get_all_obj_of_type(model.ConceptDescription) concept_descriptions, cursor = self._get_slice(request, concept_descriptions) - return response_t(list(concept_descriptions), cursor=cursor, stripped=is_stripped_request(request)) + return response_t(list(concept_descriptions), paging_metadata=cursor, stripped=is_stripped_request(request)) def post_concept_description( self, request: Request, url_args: Dict, response_t: Type[APIResponse], map_adapter: MapAdapter From 1a609627b070c85cab1495aa9fa65223c952dc44 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 12 May 2026 17:48:01 +0200 Subject: [PATCH 03/13] fix: fix paging_metadata kwarg in discovery and registry handlers --- server/app/interfaces/discovery.py | 4 ++-- server/app/interfaces/registry.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/app/interfaces/discovery.py b/server/app/interfaces/discovery.py index a6e43361..5d33f4e8 100644 --- a/server/app/interfaces/discovery.py +++ b/server/app/interfaces/discovery.py @@ -161,7 +161,7 @@ def get_all_aas_ids_by_asset_link( matching_aas_keys.update(aas_keys) paginated_slice, cursor = self._get_slice(request, list(matching_aas_keys)) - return response_t(list(paginated_slice), cursor=cursor) + return response_t(list(paginated_slice), paging_metadata=cursor) def search_all_aas_ids_by_asset_link( self, request: Request, url_args: dict, response_t: Type[APIResponse], **_kwargs @@ -172,7 +172,7 @@ def search_all_aas_ids_by_asset_link( aas_keys = self.persistent_store.search_aas_ids_by_asset_link(asset_link) matching_aas_keys.update(aas_keys) paginated_slice, cursor = self._get_slice(request, list(matching_aas_keys)) - return response_t(list(paginated_slice), cursor=cursor) + return response_t(list(paginated_slice), paging_metadata=cursor) def get_all_specific_asset_ids_by_aas_id( self, request: Request, url_args: dict, response_t: Type[APIResponse], **_kwargs diff --git a/server/app/interfaces/registry.py b/server/app/interfaces/registry.py index 8494b629..72581b51 100644 --- a/server/app/interfaces/registry.py +++ b/server/app/interfaces/registry.py @@ -225,7 +225,7 @@ def get_all_submodel_descriptors_through_superpath( ) -> Response: aas_descriptor = self._get_aas_descriptor(url_args) submodel_descriptors, cursor = self._get_slice(request, aas_descriptor.submodel_descriptors) - return response_t(list(submodel_descriptors), cursor=cursor) + return response_t(list(submodel_descriptors), paging_metadata=cursor) def get_submodel_descriptor_by_id_through_superpath( self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs From af37711a3f04a3574b53642f8fc3badcfd32a689 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 11 May 2026 17:20:24 +0200 Subject: [PATCH 04/13] Add Neo4j backend and AASQL query endpoints for aas-demonstrator - server/app/backend/neo4j.py: factory building Neo4jObjectStore from env vars (NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD) - run_repository.py: detect STORAGE_BACKEND=neo4j, load input files into Neo4j on startup via load_directory(); skip already-existing identifiables - repository.py: POST /query/submodels (SSP-005) and POST /query/shells (SSP-003) accept AASQL JSON, compile to Cypher via convert_aasql_to_cypher(), execute against Neo4j, return QueryResultSubmodel / QueryResultAssetAdministrationShell - service_specification.py: add AAS_REPOSITORY_QUERY and SUBMODEL_REPOSITORY_QUERY profile enum values; include both in SUPPORTED_PROFILES --- server/app/backend/neo4j.py | 14 ++++++++ server/app/interfaces/repository.py | 49 +++++++++++++++++++++++++++ server/app/services/run_repository.py | 39 +++++++++++++++++---- 3 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 server/app/backend/neo4j.py diff --git a/server/app/backend/neo4j.py b/server/app/backend/neo4j.py new file mode 100644 index 00000000..d5b6b524 --- /dev/null +++ b/server/app/backend/neo4j.py @@ -0,0 +1,14 @@ +# Copyright (c) 2026 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. +# +# SPDX-License-Identifier: MIT + +from aas_mapping.aas_neo4j_adapter.aas_neo4j_client import AASNeo4JClient, AAS_NEO4J_MODEL_CONFIG +from aas_mapping.aas_neo4j_adapter.neo_aas_object_store import Neo4jObjectStore + + +def build_neo4j_object_store(uri: str, user: str, password: str) -> Neo4jObjectStore: + client = AASNeo4JClient(uri=uri, user=user, password=password, model_config=AAS_NEO4J_MODEL_CONFIG) + return Neo4jObjectStore(client=client) diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index 5e038ee5..9fa19534 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -32,6 +32,8 @@ ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_FULL, ServiceSpecificationProfileEnum.AAS_REPOSITORY_READ, ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_READ, + ServiceSpecificationProfileEnum.AAS_REPOSITORY_QUERY, + ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_QUERY, ]) @@ -49,6 +51,8 @@ def __init__( Submount( base_path, [ + Rule("/query/shells", methods=["POST"], endpoint=self.query_shells), + Rule("/query/submodels", methods=["POST"], endpoint=self.query_submodels), Rule("/serialization", methods=["GET"], endpoint=self.not_implemented), Rule("/description", methods=["GET"], endpoint=self.get_description), Rule("/shells", methods=["GET"], endpoint=self.get_aas_all), @@ -517,6 +521,51 @@ def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) -> model def _get_concept_description(self, url_args): return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) + # ------ AASQL QUERY ROUTES ------- + def _query_neo4j(self, request: Request, return_var: str) -> List[Dict]: + try: + from aas_mapping.aas_neo4j_adapter.neo_aas_object_store import Neo4jObjectStore + from aas_mapping.aas_neo4j_adapter.querification.aasql_to_cypher import convert_aasql_to_cypher + except ImportError as e: + raise werkzeug.exceptions.NotImplemented("aas_mapping package required for query endpoints") from e + + if not isinstance(self.object_store, Neo4jObjectStore): + raise werkzeug.exceptions.NotImplemented( + "Query endpoints require Neo4j backend (set STORAGE_BACKEND=neo4j)" + ) + + client = self.object_store._client + try: + cypher = convert_aasql_to_cypher(request.get_data(as_text=True)) + except (json.JSONDecodeError, ValueError) as e: + raise BadRequest(f"Invalid AASQL query: {e}") from e + + records = client.execute_clause(cypher) or [] + results = [] + for record in records: + if return_var in record.keys(): + obj_id = record[return_var]["id"] + elif f"{return_var}.id" in record.keys(): + obj_id = record[f"{return_var}.id"] + else: + continue + results.append(client.get_identifiable(obj_id)) + return results + + def query_submodels(self, request: Request, url_args: Dict, **_kwargs) -> Response: + results = self._query_neo4j(request, "sm") + return Response( + json.dumps({"paging_metadata": {"resultType": "Submodel"}, "result": results}), + content_type="application/json", + ) + + def query_shells(self, request: Request, url_args: Dict, **_kwargs) -> Response: + results = self._query_neo4j(request, "aas") + return Response( + json.dumps({"paging_metadata": {"resultType": "AssetAdministrationShell"}, "result": results}), + content_type="application/json", + ) + # ------ all not implemented ROUTES ------- def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: raise werkzeug.exceptions.NotImplemented("This route is not implemented!") diff --git a/server/app/services/run_repository.py b/server/app/services/run_repository.py index 04e6c744..b69e412d 100644 --- a/server/app/services/run_repository.py +++ b/server/app/services/run_repository.py @@ -15,6 +15,7 @@ from basyx.aas.adapter import load_directory from basyx.aas.adapter.aasx import DictSupplementaryFileContainer from basyx.aas.backend.local_file import LocalFileIdentifiableStore +from basyx.aas.model import AbstractObjectStore from basyx.aas.model.provider import DictIdentifiableStore from app.interfaces.repository import WSGIApp @@ -42,7 +43,7 @@ def setup_logger() -> logging.Logger: def build_storage( env_input: str, env_storage: str, env_storage_persistency: bool, env_storage_overwrite: bool, logger: logging.Logger -) -> Tuple[Union[DictIdentifiableStore, LocalFileIdentifiableStore], DictSupplementaryFileContainer]: +) -> Tuple[AbstractObjectStore, DictSupplementaryFileContainer]: """ Configure the server's storage according to the given start-up settings. @@ -53,12 +54,37 @@ def build_storage( :param env_storage_overwrite: Flag to overwrite existing :class:`Identifiables ` in the :class:`~basyx.aas.backend.local_file.LocalFileIdentifiableStore` if persistent storage is enabled :param logger: :class:`~logging.Logger` used for start-up diagnostics - :return: Tuple consisting of a :class:`~basyx.aas.model.provider.DictIdentifiableStore` if persistent storage is - disabled or a :class:`~basyx.aas.backend.local_file.LocalFileIdentifiableStore` if persistent storage is - enabled and a :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer` as storage for - :class:`~interfaces.repository.WSGIApp` + :return: Tuple consisting of a storage backend and a + :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer` for :class:`~interfaces.repository.WSGIApp` """ + env_storage_backend = os.getenv("STORAGE_BACKEND", "memory").lower() + + if env_storage_backend == "neo4j": + from app.backend.neo4j import build_neo4j_object_store + neo4j_uri = os.getenv("NEO4J_URI", "bolt://localhost:7687") + neo4j_user = os.getenv("NEO4J_USER", "neo4j") + neo4j_password = os.getenv("NEO4J_PASSWORD", "") + logger.info('Using Neo4j backend at "%s" (user=%s)', neo4j_uri, neo4j_user) + store = build_neo4j_object_store(neo4j_uri, neo4j_user, neo4j_password) + if os.path.isdir(env_input): + input_objects, input_supp_files = load_directory(env_input) + loaded, skipped = 0, 0 + for obj in input_objects: + try: + store.add(obj) + loaded += 1 + except KeyError: + skipped += 1 + logger.info( + 'Loaded %d identifiable(s) from "%s" into Neo4j (%d skipped, already existed)', + loaded, env_input, skipped, + ) + return store, input_supp_files + else: + logger.warning('INPUT directory "%s" not found, starting empty Neo4j store', env_input) + return store, DictSupplementaryFileContainer() + if env_storage_persistency: storage_files = LocalFileIdentifiableStore(env_storage) storage_files.check_directory(create=True) @@ -109,8 +135,9 @@ def build_storage( wsgi_optparams = {"base_path": env_api_base_path} if env_api_base_path else {} logger.info( - 'Loaded settings API_BASE_PATH="%s", INPUT="%s", STORAGE="%s", PERSISTENCY=%s, OVERWRITE=%s', + 'Loaded settings API_BASE_PATH="%s", STORAGE_BACKEND="%s", INPUT="%s", STORAGE="%s", PERSISTENCY=%s, OVERWRITE=%s', env_api_base_path or "", + os.getenv("STORAGE_BACKEND", "memory"), env_input, env_storage, env_storage_persistency, From 00bb629c4a941831c3276f96f0ecb6076e023c1f Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 11 May 2026 18:07:01 +0200 Subject: [PATCH 05/13] Fix per-file loading in Neo4j build_storage to handle duplicate IDs load_directory() accumulates all files into one DictIdentifiableStore which raises KeyError on duplicate identifiable IDs. Load each file into a separate store, then add to Neo4j skipping duplicates. --- server/app/services/run_repository.py | 34 +++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/server/app/services/run_repository.py b/server/app/services/run_repository.py index b69e412d..f7873567 100644 --- a/server/app/services/run_repository.py +++ b/server/app/services/run_repository.py @@ -68,14 +68,34 @@ def build_storage( logger.info('Using Neo4j backend at "%s" (user=%s)', neo4j_uri, neo4j_user) store = build_neo4j_object_store(neo4j_uri, neo4j_user, neo4j_password) if os.path.isdir(env_input): - input_objects, input_supp_files = load_directory(env_input) + from pathlib import Path + from basyx.aas.adapter import read_aas_json_file_into, read_aas_xml_file_into + from basyx.aas.adapter.aasx import AASXReader + from basyx.aas.model.provider import DictIdentifiableStore as _FileStore + input_supp_files = DictSupplementaryFileContainer() loaded, skipped = 0, 0 - for obj in input_objects: - try: - store.add(obj) - loaded += 1 - except KeyError: - skipped += 1 + for file in Path(env_input).iterdir(): + if not file.is_file(): + continue + file_store: _FileStore = _FileStore() + suffix = file.suffix.lower() + if suffix == ".json": + with open(file) as f: + read_aas_json_file_into(file_store, f) + elif suffix == ".xml": + with open(file) as f: + read_aas_xml_file_into(file_store, f) + elif suffix == ".aasx": + with AASXReader(file) as reader: + reader.read_into(object_store=file_store, file_store=input_supp_files) + else: + continue + for obj in file_store: + try: + store.add(obj) + loaded += 1 + except KeyError: + skipped += 1 logger.info( 'Loaded %d identifiable(s) from "%s" into Neo4j (%d skipped, already existed)', loaded, env_input, skipped, From 120355b55f06433490265112a0b75813cbabbb79 Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 11 May 2026 23:34:21 +0200 Subject: [PATCH 06/13] Add ServiceUnavailable retry loop in Neo4j build_storage Docker container DNS resolution may transiently fail with [Errno -3] immediately after container start before the network namespace is fully configured. Retry up to 10 times with 3s delay on ServiceUnavailable before giving up. --- server/app/services/run_repository.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/server/app/services/run_repository.py b/server/app/services/run_repository.py index f7873567..1c5066c5 100644 --- a/server/app/services/run_repository.py +++ b/server/app/services/run_repository.py @@ -10,6 +10,7 @@ import logging import os +import time from typing import Tuple, Union from basyx.aas.adapter import load_directory @@ -72,8 +73,10 @@ def build_storage( from basyx.aas.adapter import read_aas_json_file_into, read_aas_xml_file_into from basyx.aas.adapter.aasx import AASXReader from basyx.aas.model.provider import DictIdentifiableStore as _FileStore + from neo4j.exceptions import ServiceUnavailable input_supp_files = DictSupplementaryFileContainer() loaded, skipped = 0, 0 + input_objects: list = [] for file in Path(env_input).iterdir(): if not file.is_file(): continue @@ -90,12 +93,22 @@ def build_storage( reader.read_into(object_store=file_store, file_store=input_supp_files) else: continue - for obj in file_store: - try: - store.add(obj) - loaded += 1 - except KeyError: - skipped += 1 + input_objects.extend(file_store) + for attempt in range(10): + try: + for obj in input_objects: + try: + store.add(obj) + loaded += 1 + except KeyError: + skipped += 1 + break + except ServiceUnavailable as exc: + if attempt == 9: + raise + logger.warning("Neo4j not reachable (%s), retrying in 3s (%d/9)...", exc, attempt + 1) + loaded, skipped = 0, 0 + time.sleep(3) logger.info( 'Loaded %d identifiable(s) from "%s" into Neo4j (%d skipped, already existed)', loaded, env_input, skipped, From cc6f30cdbdbdb78cd34d33940474c1a43f701d40 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 12 May 2026 20:26:11 +0200 Subject: [PATCH 07/13] Remove Neo4j/AASQL code from basyx-python-sdk; delegate to aas4graph plugin Neo4j-specific logic moved to aas_mapping/server/ in aas4graph (neo4aas): - Delete server/app/backend/neo4j.py (now in aas4graph, copied by Dockerfile) - run_repository.py: replace inline Neo4j block with plugin call build_neo4j_storage(env_input, logger) from app.backend.neo4j - repository.py: remove _query_neo4j / query_shells / query_submodels methods, remove /query/shells and /query/submodels URL rules, remove AAS_REPOSITORY_QUERY / SUBMODEL_REPOSITORY_QUERY from SUPPORTED_PROFILES (advertised only by Neo4jWSGIApp in aas4graph) --- server/app/backend/neo4j.py | 14 ------- server/app/interfaces/repository.py | 49 ---------------------- server/app/services/run_repository.py | 58 +-------------------------- 3 files changed, 2 insertions(+), 119 deletions(-) delete mode 100644 server/app/backend/neo4j.py diff --git a/server/app/backend/neo4j.py b/server/app/backend/neo4j.py deleted file mode 100644 index d5b6b524..00000000 --- a/server/app/backend/neo4j.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2026 the Eclipse BaSyx Authors -# -# This program and the accompanying materials are made available under the terms of the MIT License, available in -# the LICENSE file of this project. -# -# SPDX-License-Identifier: MIT - -from aas_mapping.aas_neo4j_adapter.aas_neo4j_client import AASNeo4JClient, AAS_NEO4J_MODEL_CONFIG -from aas_mapping.aas_neo4j_adapter.neo_aas_object_store import Neo4jObjectStore - - -def build_neo4j_object_store(uri: str, user: str, password: str) -> Neo4jObjectStore: - client = AASNeo4JClient(uri=uri, user=user, password=password, model_config=AAS_NEO4J_MODEL_CONFIG) - return Neo4jObjectStore(client=client) diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index 9fa19534..5e038ee5 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -32,8 +32,6 @@ ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_FULL, ServiceSpecificationProfileEnum.AAS_REPOSITORY_READ, ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_READ, - ServiceSpecificationProfileEnum.AAS_REPOSITORY_QUERY, - ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_QUERY, ]) @@ -51,8 +49,6 @@ def __init__( Submount( base_path, [ - Rule("/query/shells", methods=["POST"], endpoint=self.query_shells), - Rule("/query/submodels", methods=["POST"], endpoint=self.query_submodels), Rule("/serialization", methods=["GET"], endpoint=self.not_implemented), Rule("/description", methods=["GET"], endpoint=self.get_description), Rule("/shells", methods=["GET"], endpoint=self.get_aas_all), @@ -521,51 +517,6 @@ def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) -> model def _get_concept_description(self, url_args): return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) - # ------ AASQL QUERY ROUTES ------- - def _query_neo4j(self, request: Request, return_var: str) -> List[Dict]: - try: - from aas_mapping.aas_neo4j_adapter.neo_aas_object_store import Neo4jObjectStore - from aas_mapping.aas_neo4j_adapter.querification.aasql_to_cypher import convert_aasql_to_cypher - except ImportError as e: - raise werkzeug.exceptions.NotImplemented("aas_mapping package required for query endpoints") from e - - if not isinstance(self.object_store, Neo4jObjectStore): - raise werkzeug.exceptions.NotImplemented( - "Query endpoints require Neo4j backend (set STORAGE_BACKEND=neo4j)" - ) - - client = self.object_store._client - try: - cypher = convert_aasql_to_cypher(request.get_data(as_text=True)) - except (json.JSONDecodeError, ValueError) as e: - raise BadRequest(f"Invalid AASQL query: {e}") from e - - records = client.execute_clause(cypher) or [] - results = [] - for record in records: - if return_var in record.keys(): - obj_id = record[return_var]["id"] - elif f"{return_var}.id" in record.keys(): - obj_id = record[f"{return_var}.id"] - else: - continue - results.append(client.get_identifiable(obj_id)) - return results - - def query_submodels(self, request: Request, url_args: Dict, **_kwargs) -> Response: - results = self._query_neo4j(request, "sm") - return Response( - json.dumps({"paging_metadata": {"resultType": "Submodel"}, "result": results}), - content_type="application/json", - ) - - def query_shells(self, request: Request, url_args: Dict, **_kwargs) -> Response: - results = self._query_neo4j(request, "aas") - return Response( - json.dumps({"paging_metadata": {"resultType": "AssetAdministrationShell"}, "result": results}), - content_type="application/json", - ) - # ------ all not implemented ROUTES ------- def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response: raise werkzeug.exceptions.NotImplemented("This route is not implemented!") diff --git a/server/app/services/run_repository.py b/server/app/services/run_repository.py index 1c5066c5..31332182 100644 --- a/server/app/services/run_repository.py +++ b/server/app/services/run_repository.py @@ -10,7 +10,6 @@ import logging import os -import time from typing import Tuple, Union from basyx.aas.adapter import load_directory @@ -62,61 +61,8 @@ def build_storage( env_storage_backend = os.getenv("STORAGE_BACKEND", "memory").lower() if env_storage_backend == "neo4j": - from app.backend.neo4j import build_neo4j_object_store - neo4j_uri = os.getenv("NEO4J_URI", "bolt://localhost:7687") - neo4j_user = os.getenv("NEO4J_USER", "neo4j") - neo4j_password = os.getenv("NEO4J_PASSWORD", "") - logger.info('Using Neo4j backend at "%s" (user=%s)', neo4j_uri, neo4j_user) - store = build_neo4j_object_store(neo4j_uri, neo4j_user, neo4j_password) - if os.path.isdir(env_input): - from pathlib import Path - from basyx.aas.adapter import read_aas_json_file_into, read_aas_xml_file_into - from basyx.aas.adapter.aasx import AASXReader - from basyx.aas.model.provider import DictIdentifiableStore as _FileStore - from neo4j.exceptions import ServiceUnavailable - input_supp_files = DictSupplementaryFileContainer() - loaded, skipped = 0, 0 - input_objects: list = [] - for file in Path(env_input).iterdir(): - if not file.is_file(): - continue - file_store: _FileStore = _FileStore() - suffix = file.suffix.lower() - if suffix == ".json": - with open(file) as f: - read_aas_json_file_into(file_store, f) - elif suffix == ".xml": - with open(file) as f: - read_aas_xml_file_into(file_store, f) - elif suffix == ".aasx": - with AASXReader(file) as reader: - reader.read_into(object_store=file_store, file_store=input_supp_files) - else: - continue - input_objects.extend(file_store) - for attempt in range(10): - try: - for obj in input_objects: - try: - store.add(obj) - loaded += 1 - except KeyError: - skipped += 1 - break - except ServiceUnavailable as exc: - if attempt == 9: - raise - logger.warning("Neo4j not reachable (%s), retrying in 3s (%d/9)...", exc, attempt + 1) - loaded, skipped = 0, 0 - time.sleep(3) - logger.info( - 'Loaded %d identifiable(s) from "%s" into Neo4j (%d skipped, already existed)', - loaded, env_input, skipped, - ) - return store, input_supp_files - else: - logger.warning('INPUT directory "%s" not found, starting empty Neo4j store', env_input) - return store, DictSupplementaryFileContainer() + from app.backend.neo4j import build_neo4j_storage + return build_neo4j_storage(env_input, logger) if env_storage_persistency: storage_files = LocalFileIdentifiableStore(env_storage) From 970750a2e4725d552bdea3c0df6d32c1b7a98997 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 12 May 2026 20:50:02 +0200 Subject: [PATCH 08/13] Remove STORAGE_BACKEND dispatch from run_repository.py; Neo4j handled by aas4graph entrypoint --- server/app/services/run_repository.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/server/app/services/run_repository.py b/server/app/services/run_repository.py index 31332182..c0a66eab 100644 --- a/server/app/services/run_repository.py +++ b/server/app/services/run_repository.py @@ -58,12 +58,6 @@ def build_storage( :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer` for :class:`~interfaces.repository.WSGIApp` """ - env_storage_backend = os.getenv("STORAGE_BACKEND", "memory").lower() - - if env_storage_backend == "neo4j": - from app.backend.neo4j import build_neo4j_storage - return build_neo4j_storage(env_input, logger) - if env_storage_persistency: storage_files = LocalFileIdentifiableStore(env_storage) storage_files.check_directory(create=True) @@ -114,9 +108,8 @@ def build_storage( wsgi_optparams = {"base_path": env_api_base_path} if env_api_base_path else {} logger.info( - 'Loaded settings API_BASE_PATH="%s", STORAGE_BACKEND="%s", INPUT="%s", STORAGE="%s", PERSISTENCY=%s, OVERWRITE=%s', + 'Loaded settings API_BASE_PATH="%s", INPUT="%s", STORAGE="%s", PERSISTENCY=%s, OVERWRITE=%s', env_api_base_path or "", - os.getenv("STORAGE_BACKEND", "memory"), env_input, env_storage, env_storage_persistency, From b705fdb2743e70382bba8fe893a33eaf006ee95e Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 12 May 2026 21:01:18 +0200 Subject: [PATCH 09/13] Revert run_repository.py to upstream develop state --- server/app/services/run_repository.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/server/app/services/run_repository.py b/server/app/services/run_repository.py index c0a66eab..04e6c744 100644 --- a/server/app/services/run_repository.py +++ b/server/app/services/run_repository.py @@ -15,7 +15,6 @@ from basyx.aas.adapter import load_directory from basyx.aas.adapter.aasx import DictSupplementaryFileContainer from basyx.aas.backend.local_file import LocalFileIdentifiableStore -from basyx.aas.model import AbstractObjectStore from basyx.aas.model.provider import DictIdentifiableStore from app.interfaces.repository import WSGIApp @@ -43,7 +42,7 @@ def setup_logger() -> logging.Logger: def build_storage( env_input: str, env_storage: str, env_storage_persistency: bool, env_storage_overwrite: bool, logger: logging.Logger -) -> Tuple[AbstractObjectStore, DictSupplementaryFileContainer]: +) -> Tuple[Union[DictIdentifiableStore, LocalFileIdentifiableStore], DictSupplementaryFileContainer]: """ Configure the server's storage according to the given start-up settings. @@ -54,8 +53,10 @@ def build_storage( :param env_storage_overwrite: Flag to overwrite existing :class:`Identifiables ` in the :class:`~basyx.aas.backend.local_file.LocalFileIdentifiableStore` if persistent storage is enabled :param logger: :class:`~logging.Logger` used for start-up diagnostics - :return: Tuple consisting of a storage backend and a - :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer` for :class:`~interfaces.repository.WSGIApp` + :return: Tuple consisting of a :class:`~basyx.aas.model.provider.DictIdentifiableStore` if persistent storage is + disabled or a :class:`~basyx.aas.backend.local_file.LocalFileIdentifiableStore` if persistent storage is + enabled and a :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer` as storage for + :class:`~interfaces.repository.WSGIApp` """ if env_storage_persistency: From 34be932363074e97fad8b3921687a49015b2d9f5 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 12 May 2026 23:32:30 +0200 Subject: [PATCH 10/13] Add QueryableObjectStore protocol and /query/shells, /query/submodels routes Introduce @runtime_checkable QueryableObjectStore Protocol in base.py so object stores can opt into AASQL query support without explicit inheritance. WSGIApp gains POST /query/shells and /query/submodels endpoints that delegate to object_store.query(). get_description() dynamically advertises query profiles when the store satisfies the protocol. --- server/app/interfaces/base.py | 20 ++++++++++++++- server/app/interfaces/repository.py | 38 +++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/server/app/interfaces/base.py b/server/app/interfaces/base.py index d3231237..865b599b 100644 --- a/server/app/interfaces/base.py +++ b/server/app/interfaces/base.py @@ -10,7 +10,7 @@ import io import itertools import json -from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Type, TypeVar, Union +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Protocol, Tuple, Type, TypeVar, Union, runtime_checkable import werkzeug.exceptions import werkzeug.routing @@ -295,6 +295,24 @@ def http_exception_to_response( return response_type(result, status=exception.code, headers=headers) +@runtime_checkable +class QueryableObjectStore(Protocol): + """Structural protocol for object stores that support AASQL querying. + + Implement ``query(aasql_body, return_var)`` to advertise query support. + No explicit inheritance required — duck-typing via ``isinstance`` works at runtime. + """ + + def query(self, aasql_body: str, return_var: str) -> List[dict]: + """Execute an AASQL query and return matching serialized AAS objects. + + :param aasql_body: raw AASQL JSON string + :param return_var: Cypher return variable name (``"sm"`` or ``"aas"``) + :return: list of serialized AAS/Submodel dicts + """ + ... + + class ObjectStoreWSGIApp(BaseWSGIApp): object_store: AbstractObjectStore diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index 5e038ee5..6cfce043 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -24,7 +24,7 @@ from app.interfaces.base import PagingMetadata from app.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, QueryableObjectStore, T from app.model import ServiceSpecificationProfileEnum, ServiceDescription SUPPORTED_PROFILES: ServiceDescription = ServiceDescription([ @@ -32,6 +32,8 @@ ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_FULL, ServiceSpecificationProfileEnum.AAS_REPOSITORY_READ, ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_READ, + # ServiceSpecificationProfileEnum.AAS_REPOSITORY_QUERY, + # ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_QUERY, ]) @@ -49,6 +51,8 @@ def __init__( Submount( base_path, [ + Rule("/query/shells", methods=["POST"], endpoint=self.query_shells), + Rule("/query/submodels", methods=["POST"], endpoint=self.query_submodels), Rule("/serialization", methods=["GET"], endpoint=self.not_implemented), Rule("/description", methods=["GET"], endpoint=self.get_description), Rule("/shells", methods=["GET"], endpoint=self.get_aas_all), @@ -517,12 +521,42 @@ def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) -> model def _get_concept_description(self, url_args): return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription) + def query_submodels(self, request: Request, url_args: Dict, **_kwargs) -> Response: + if not isinstance(self.object_store, QueryableObjectStore): + raise werkzeug.exceptions.NotImplemented("The current store does not support AASQL queries") + try: + results = self.object_store.query(request.get_data(as_text=True), "sm") + except (json.JSONDecodeError, ValueError) as e: + raise BadRequest(f"Invalid AASQL query: {e}") from e + return Response( + json.dumps({"paging_metadata": {"resultType": "Submodel"}, "result": results}), + content_type="application/json", + ) + + def query_shells(self, request: Request, url_args: Dict, **_kwargs) -> Response: + if not isinstance(self.object_store, QueryableObjectStore): + raise werkzeug.exceptions.NotImplemented("The current store does not support AASQL queries") + try: + results = self.object_store.query(request.get_data(as_text=True), "aas") + except (json.JSONDecodeError, ValueError) as e: + raise BadRequest(f"Invalid AASQL query: {e}") from e + return Response( + json.dumps({"paging_metadata": {"resultType": "AssetAdministrationShell"}, "result": results}), + content_type="application/json", + ) + # ------ all not implemented ROUTES ------- 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: - return response_t(SUPPORTED_PROFILES.to_dict()) + profiles = SUPPORTED_PROFILES.to_dict() + if isinstance(self.object_store, QueryableObjectStore): + profiles["profiles"].extend([ + ServiceSpecificationProfileEnum.AAS_REPOSITORY_QUERY.value, + ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_QUERY.value, + ]) + return response_t(profiles) # ------ AAS REPO ROUTES ------- def get_aas_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response: From fcd3371e82985ad5de882cffe28d4eaf846a6102 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 12 May 2026 23:47:02 +0200 Subject: [PATCH 11/13] Fix load_directory to handle Descriptor objects (not Identifiable) --- server/app/model/provider.py | 44 ++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/server/app/model/provider.py b/server/app/model/provider.py index 97067e7d..3f529118 100644 --- a/server/app/model/provider.py +++ b/server/app/model/provider.py @@ -1,10 +1,10 @@ +import json from pathlib import Path from typing import IO, Dict, Iterable, Iterator, Union from basyx.aas import model from basyx.aas.model import provider as sdk_provider -import app.adapter as adapter from app.model import descriptor PathOrIO = Union[Path, IO] @@ -51,29 +51,35 @@ def __iter__(self) -> Iterator[_DESCRIPTOR_TYPE]: return iter(self._backend.values()) +_DESCRIPTOR_KEY_TO_CLS = ( + ("assetAdministrationShellDescriptors", descriptor.AssetAdministrationShellDescriptor), + ("submodelDescriptors", descriptor.SubmodelDescriptor), +) + + def load_directory(directory: Union[Path, str]) -> DictDescriptorStore: """ - Create a new :class:`~basyx.aas.model.provider.DictIdentifiableStore` and use it to load Asset Administration Shell - and Submodel files in ``AASX``, ``JSON`` and ``XML`` format from a given directory into memory. Additionally, load - all embedded supplementary files into a new :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer`. - - :param directory: :class:`~pathlib.Path` or ``str`` pointing to the directory containing all Asset Administration - Shell and Submodel files to load - :return: Tuple consisting of a :class:`~basyx.aas.model.provider.DictIdentifiableStore` and a - :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer` containing all loaded data - """ + Load AAS/Submodel descriptor JSON files from a directory into a :class:`DictDescriptorStore`. - dict_descriptor_store: DictDescriptorStore = DictDescriptorStore() + :param directory: Path to the directory containing JSON descriptor files + :return: Populated :class:`DictDescriptorStore` + """ + from app.adapter import ServerAASFromJsonDecoder + store = DictDescriptorStore() directory = Path(directory) for file in directory.iterdir(): - if not file.is_file(): + if not file.is_file() or file.suffix.lower() != ".json": continue - - suffix = file.suffix.lower() - if suffix == ".json": - with open(file) as f: - adapter.read_server_aas_json_file_into(dict_descriptor_store, f) - - return dict_descriptor_store + with open(file) as f: + data = json.load(f, cls=ServerAASFromJsonDecoder) + for key, cls in _DESCRIPTOR_KEY_TO_CLS: + for item in data.get(key, []): + if isinstance(item, cls): + try: + store.add(item) + except KeyError: + pass + + return store From 9da157c04d9e2a0d9f5dae4b72de4466557c135f Mon Sep 17 00:00:00 2001 From: zrgt Date: Mon, 15 Jun 2026 15:31:47 +0200 Subject: [PATCH 12/13] Align load_directory import style with develop (module-level app.adapter) --- server/app/model/provider.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/app/model/provider.py b/server/app/model/provider.py index 3f529118..8bef91e5 100644 --- a/server/app/model/provider.py +++ b/server/app/model/provider.py @@ -5,6 +5,7 @@ from basyx.aas import model from basyx.aas.model import provider as sdk_provider +from app import adapter from app.model import descriptor PathOrIO = Union[Path, IO] @@ -64,8 +65,6 @@ def load_directory(directory: Union[Path, str]) -> DictDescriptorStore: :param directory: Path to the directory containing JSON descriptor files :return: Populated :class:`DictDescriptorStore` """ - from app.adapter import ServerAASFromJsonDecoder - store = DictDescriptorStore() directory = Path(directory) @@ -73,7 +72,7 @@ def load_directory(directory: Union[Path, str]) -> DictDescriptorStore: if not file.is_file() or file.suffix.lower() != ".json": continue with open(file) as f: - data = json.load(f, cls=ServerAASFromJsonDecoder) + data = json.load(f, cls=adapter.ServerAASFromJsonDecoder) for key, cls in _DESCRIPTOR_KEY_TO_CLS: for item in data.get(key, []): if isinstance(item, cls): From b8bdd93f69c63cf40bb71d7ad9f5c38530bb631a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Farkas?= <109728311+tamasf1@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:35:20 +0200 Subject: [PATCH 13/13] Fix PUT/DELETE submodel-element-by-id-short-path not persisting changes (#80) * Fix PUT/DELETE submodel-element-by-id-short-path not persisting changes put_submodel_submodel_elements_id_short_path and delete_submodel_submodel_elements_id_short_path each fetched the submodel twice: once (indirectly) to locate/mutate the target element, and again to pass to object_store.commit(). For object stores that deserialize a fresh object graph on every get (e.g. Neo4jObjectStore), these are two distinct instances, so the mutation made on the first instance was never part of the object passed to commit() and the change was silently lost. Fetch the submodel once, navigate to the nested element within that same instance, mutate it, and commit that same submodel instance. --- server/app/interfaces/repository.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index 38dbe53e..59622920 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -830,23 +830,25 @@ def post_submodel_submodel_elements_id_short_path( def put_submodel_submodel_elements_id_short_path( self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs ) -> Response: - submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + submodel = self._get_submodel(url_args) + submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 new_submodel_element = HTTPApiDecoder.request_body( request, model.SubmodelElement, is_stripped_request(request) # type: ignore[type-abstract] ) submodel_element.update_from(new_submodel_element) - self.object_store.commit(self._get_submodel(url_args)) + self.object_store.commit(submodel) return response_t() def delete_submodel_submodel_elements_id_short_path( self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs ) -> Response: - sm_or_se = self._get_submodel_or_nested_submodel_element(url_args) + submodel = self._get_submodel(url_args) + sm_or_se = self._get_nested_submodel_element(submodel, url_args["id_shorts"]) parent: model.UniqueIdShortNamespace = self._expect_namespace(sm_or_se.parent, sm_or_se.id_short) self._namespace_submodel_element_op(parent, parent.remove_referable, sm_or_se.id_short) - self.object_store.commit(self._get_submodel(url_args)) + self.object_store.commit(submodel) return response_t() def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) -> Response: