Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
ce642c1
Füge experimentelle Änderungen für Registry und Discovery hinzu
Ornella33 Mar 25, 2025
7204ae2
Remove test.py from repository and add it to .gitignore
Ornella33 Mar 31, 2025
25cf282
correct discovery server implementation
Ornella33 Apr 1, 2025
b68efaa
remove unused code
Ornella33 Apr 2, 2025
b590e1c
add in-memory storage and adapt README
Ornella33 Apr 14, 2025
1b676e7
change main.py and disccovery.py
Ornella33 Apr 14, 2025
7cff8cf
Extract server-related components into server app
zrgt Apr 15, 2025
a6577be
Refactor `_get_aas_class_parsers`
zrgt Apr 15, 2025
11c59bc
fix aas_descriptor construct method
Ornella33 Apr 15, 2025
6d4aab1
Refactor `read_aas_json_file_into`
zrgt Apr 15, 2025
a34230f
Refactor `default()`
zrgt Apr 15, 2025
9079d82
fix method update_from
Ornella33 Apr 15, 2025
bd2a9c7
Merge remote-tracking branch 'rwth-iat/Experimental/server_app' into …
zrgt Apr 15, 2025
a366538
Refactor `_create_dict()`
zrgt Apr 15, 2025
72297f4
Remove `jsonization._create_dict` as not used
zrgt Apr 15, 2025
bd48dec
Split `http.py` into `repository` and `http_api_helpers`
zrgt Apr 15, 2025
6fd1612
Refactor server_model and move create interfaces folder
zrgt Apr 16, 2025
eba1d89
Refactor `result_to_xml` and `message_to_xml`
zrgt Apr 16, 2025
4acab0d
Move all response related to `response.py`
zrgt Apr 16, 2025
567b5f1
Create base classes for WSGI apps
zrgt Apr 16, 2025
3d15b51
Refactor `http_api_helpers.py` and `response.py`
zrgt Apr 16, 2025
cb107ed
Reformat code with PyCharm
zrgt Apr 16, 2025
4e1c647
Small fixes
zrgt Apr 17, 2025
95b2d5a
Refactor
zrgt Apr 17, 2025
b65c420
Refactor
zrgt Apr 17, 2025
b0f79d6
Refactor some methods in registry.py and fix some typos
Ornella33 Apr 17, 2025
7c8fbe2
remove xmlization for Registry and Discovery classes
Ornella33 Apr 17, 2025
eb44e8a
change according to xmlization removal for registry and discovery cla…
Ornella33 Apr 17, 2025
d608409
fix error with ServerAASToJSONEncoder
Ornella33 Apr 22, 2025
dde2499
Refactor `response.py`
zrgt Apr 23, 2025
df38540
Refactor utils
zrgt Apr 23, 2025
1da157f
Rename `server_model` to `model`
zrgt Apr 23, 2025
a96da47
correct typos from renaming server_model to model
Ornella33 Apr 24, 2025
115db62
Remove discovery/registry related code
zrgt May 22, 2025
65d1918
Merge remote-tracking branch 'rwth-iat/develop' into refactor/server
zrgt May 22, 2025
0c36396
Add missing code from PR #362
zrgt May 22, 2025
6b3c646
Revert changes in .gitignore
zrgt May 22, 2025
0a8546e
Fix copyright
zrgt May 22, 2025
bfd1411
Refactor `test_http.py` to `test_repository.py`
zrgt May 22, 2025
1fd76de
fix copyright
Frosty2500 May 29, 2025
a783066
fix MyPy errors, some tests
Frosty2500 May 29, 2025
66f3320
fix bugs, reintroduce Identifiable check
Frosty2500 Jun 3, 2025
3226718
Revert "Remove discovery/registry related code"
zrgt Jun 17, 2025
1268f6a
correct json serialisation for AASDescriptor
Ornella33 Apr 24, 2025
74b64d2
adapt filter options for get_all_aas_descritors and remove filter for…
Ornella33 Apr 25, 2025
b64d589
add service description
Ornella33 Apr 25, 2025
e494d3c
Merge remote-tracking branch 'rwth-iat/refactor/server' into refactor…
zrgt Jun 17, 2025
1de92b3
clean code
Ornella33 Jun 23, 2025
59748cf
add README and docker deployment for registry
Ornella33 Jul 8, 2025
a7efefc
remove files from another branch
Ornella33 Jul 8, 2025
d3d4dbb
Stop tracking unnecessary files
Ornella33 Jul 8, 2025
673f18d
Update README
Ornella33 Jul 15, 2025
a581603
add docker deployment for discovery service
Ornella33 Jul 15, 2025
42dd189
Update repository
Ornella33 Jul 15, 2025
d37bc01
Ignore test.py
Ornella33 Jul 15, 2025
d907b01
Merge remote-tracking branch 'origin/develop' into experimental/regis…
Ornella33 Jul 15, 2025
ac8f7f5
Merge branch 'main' into develop
s-heppner Jul 30, 2025
974e112
Merge branch 'eclipse-basyx:develop' into develop
Frosty2500 Sep 23, 2025
5c5e1ae
Merge remote-tracking branch 'origin/develop' into experimental/regis…
Ornella33 Oct 21, 2025
01767c1
add init
Ornella33 Feb 24, 2026
81994f1
Merge develop
Ornella33 Feb 24, 2026
11669cf
Resolve import conflict
Ornella33 Feb 24, 2026
f8985e1
Merge remote-tracking branch 'origin/develop' into experimental/regis…
Ornella33 Mar 1, 2026
12d64bd
remove import errors
Ornella33 Mar 1, 2026
70448ae
remove init file
Ornella33 Mar 1, 2026
f885bce
correct errors
Ornella33 Mar 1, 2026
5389ab4
Add DescriptorStores (#75)
s-heppner Mar 11, 2026
d2761de
Delete unnecessary files descriptorStore.py and provider.py
Ornella33 Mar 11, 2026
990e03a
Fix DiscoveryStore and remove MongoDB dependency
s-heppner Mar 11, 2026
298b495
Fix Descriptor types for DescriptorStores
s-heppner Mar 11, 2026
1aab8c2
update discovery
Ornella33 Mar 12, 2026
1287a43
add gitignore
Ornella33 Mar 12, 2026
e691383
Ignore server/storage dicrectory
Ornella33 Mar 13, 2026
27d71e8
Update repository.py from develop branch
Ornella33 Mar 13, 2026
21f89c4
Update Registry
Ornella33 Mar 13, 2026
8da14be
Update gitignore
Ornella33 Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,7 @@ compliance_tool/aas_compliance_tool/version.py
server/app/version.py

# Ignore the content of the server storage
server/input/
server/example_configurations/repository_standalone/input/
server/example_configurations/repository_standalone/storage/
/storage/
server/storage/
Empty file added server/app/adapter/__init__.py
Empty file.
336 changes: 336 additions & 0 deletions server/app/adapter/jsonization.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions server/app/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .local_file import *
176 changes: 176 additions & 0 deletions server/app/backend/local_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from typing import Iterator, Dict, Type, Union
import logging
import json
import os
import hashlib
import threading
import weakref

from app.model import AssetAdministrationShellDescriptor, SubmodelDescriptor
from basyx.aas import model
from basyx.aas.model import provider as sdk_provider

from app.model import descriptor
from app.adapter import jsonization


logger = logging.getLogger(__name__)

_DESCRIPTOR_TYPE = Union[descriptor.AssetAdministrationShellDescriptor, descriptor.SubmodelDescriptor]
_DESCRIPTOR_CLASSES = (descriptor.AssetAdministrationShellDescriptor, descriptor.SubmodelDescriptor)

# We need to resolve the Descriptor type in order to deserialize it again from JSON
DESCRIPTOR_TYPE_TO_STRING: Dict[Type[Union[AssetAdministrationShellDescriptor, SubmodelDescriptor]], str] = {
AssetAdministrationShellDescriptor: "AssetAdministrationShellDescriptor",
SubmodelDescriptor: "SubmodelDescriptor",
}


class LocalFileDescriptorStore(sdk_provider.AbstractObjectStore[model.Identifier, _DESCRIPTOR_TYPE]):
"""
An ObjectStore implementation for :class:`~app.model.descriptor.Descriptor` BaSyx Python SDK objects backed
by a local file based local backend
"""
def __init__(self, directory_path: str):
"""
Initializer of class LocalFileDescriptorStore

:param directory_path: Path to the local file backend (the path where you want to store your AAS JSON files)
"""
self.directory_path: str = directory_path.rstrip("/")

# A dictionary of weak references to local replications of stored objects. Objects are kept in this cache as
# long as there is any other reference in the Python application to them. We use this to make sure that only one
# local replication of each object is kept in the application and retrieving an object from the store always
# returns the **same** (not only equal) object. Still, objects are forgotten, when they are not referenced
# anywhere else to save memory.
self._object_cache: weakref.WeakValueDictionary[model.Identifier, _DESCRIPTOR_TYPE] \
= weakref.WeakValueDictionary()
self._object_cache_lock = threading.Lock()

def check_directory(self, create=False):
"""
Check if the directory exists and created it if not (and requested to do so)

:param create: If True and the database does not exist, try to create it
"""
if not os.path.exists(self.directory_path):
if not create:
raise FileNotFoundError("The given directory ({}) does not exist".format(self.directory_path))
# Create directory
os.mkdir(self.directory_path)
logger.info("Creating directory {}".format(self.directory_path))

def get_descriptor_by_hash(self, hash_: str) -> _DESCRIPTOR_TYPE:
"""
Retrieve an AAS Descriptor object from the local file by its identifier hash

:raises KeyError: If the respective file could not be found
"""
# Try to get the correct file
try:
with open("{}/{}.json".format(self.directory_path, hash_), "r") as file:
obj = json.load(file, cls=jsonization.ServerAASFromJsonDecoder)
except FileNotFoundError as e:
raise KeyError("No Descriptor with hash {} found in local file database".format(hash_)) from e
# If we still have a local replication of that object (since it is referenced from anywhere else), update that
# replication and return it.
with self._object_cache_lock:
if obj.id in self._object_cache:
old_obj = self._object_cache[obj.id]
old_obj.update_from(obj)
return old_obj
self._object_cache[obj.id] = obj
return obj

def get_item(self, identifier: model.Identifier) -> _DESCRIPTOR_TYPE:
"""
Retrieve an AAS Descriptor object from the local file by its :class:`~basyx.aas.model.base.Identifier`

:raises KeyError: If the respective file could not be found
"""
try:
return self.get_descriptor_by_hash(self._transform_id(identifier))
except KeyError as e:
raise KeyError("No Identifiable with id {} found in local file database".format(identifier)) from e

def add(self, x: _DESCRIPTOR_TYPE) -> None:
"""
Add a Descriptor object to the store

:raises KeyError: If an object with the same id exists already in the object store
"""
logger.debug("Adding object %s to Local File Store ...", repr(x))
if os.path.exists("{}/{}.json".format(self.directory_path, self._transform_id(x.id))):
raise KeyError("Descriptor with id {} already exists in local file database".format(x.id))
with open("{}/{}.json".format(self.directory_path, self._transform_id(x.id)), "w") as file:
# Usually, we don't need to serialize the modelType, since during HTTP requests, we know exactly if this
# is an AASDescriptor or SubmodelDescriptor. However, here we cannot distinguish them, so to deserialize
# them successfully, we hack the `modelType` into the JSON.
serialized = json.loads(
json.dumps(x, cls=jsonization.ServerAASToJsonEncoder)
)
serialized["modelType"] = DESCRIPTOR_TYPE_TO_STRING[type(x)]
json.dump(serialized, file, indent=4)
with self._object_cache_lock:
self._object_cache[x.id] = x

def discard(self, x: _DESCRIPTOR_TYPE) -> None:
"""
Delete an :class:`~app.model.descriptor.Descriptor` AAS object from the local file store

:param x: The object to be deleted
:raises KeyError: If the object does not exist in the database
"""
logger.debug("Deleting object %s from Local File Store database ...", repr(x))
try:
os.remove("{}/{}.json".format(self.directory_path, self._transform_id(x.id)))
except FileNotFoundError as e:
raise KeyError("No AAS Descriptor object with id {} exists in local file database".format(x.id)) from e
with self._object_cache_lock:
self._object_cache.pop(x.id, None)

def __contains__(self, x: object) -> bool:
"""
Check if an object with the given :class:`~basyx.aas.model.base.Identifier` or the same
:class:`~basyx.aas.model.base.Identifier` as the given object is contained in the local file database

:param x: AAS object :class:`~basyx.aas.model.base.Identifier` or :class:`~app.model.descriptor.Descriptor`
AAS object
:return: ``True`` if such an object exists in the database, ``False`` otherwise
"""
if isinstance(x, model.Identifier):
identifier = x
elif isinstance(x, _DESCRIPTOR_CLASSES):
identifier = x.id
else:
return False
logger.debug("Checking existence of Descriptor object with id %s in database ...", repr(x))
return os.path.exists("{}/{}.json".format(self.directory_path, self._transform_id(identifier)))

def __len__(self) -> int:
"""
Retrieve the number of objects in the local file database

:return: The number of objects (determined from the number of documents)
"""
logger.debug("Fetching number of documents from database ...")
return len(os.listdir(self.directory_path))

def __iter__(self) -> Iterator[_DESCRIPTOR_TYPE]:
"""
Iterate all :class:`~app.model.descriptor.Descriptor` objects in the local folder.

This method returns an iterator, containing only a list of all identifiers in the database and retrieving
the identifiable objects on the fly.
"""
logger.debug("Iterating over objects in database ...")
for name in os.listdir(self.directory_path):
yield self.get_descriptor_by_hash(name.rstrip(".json"))

@staticmethod
def _transform_id(identifier: model.Identifier) -> str:
"""
Helper method to represent an ASS Identifier as a string to be used as Local file document id
"""
return hashlib.sha256(identifier.encode("utf-8")).hexdigest()
7 changes: 3 additions & 4 deletions server/app/interfaces/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,19 +264,18 @@ def http_exception_to_response(exception: werkzeug.exceptions.HTTPException, res
class ObjectStoreWSGIApp(BaseWSGIApp):
object_store: AbstractObjectStore

def _get_all_obj_of_type(self, type_: Type[model.provider._IDENTIFIABLE]) -> Iterator[model.provider._IDENTIFIABLE]:
def _get_all_obj_of_type(self, type_: Type[T]) -> Iterator[T]:
for obj in self.object_store:
if isinstance(obj, type_):
yield obj

def _get_obj_ts(self, identifier: model.Identifier, type_: Type[model.provider._IDENTIFIABLE]) \
-> model.provider._IDENTIFIABLE:
def _get_obj_ts(self, identifier: model.Identifier, type_: Type[T]) \
-> T:
identifiable = self.object_store.get(identifier)
if not isinstance(identifiable, type_):
raise NotFound(f"No {type_.__name__} with {identifier} found!")
return identifiable


class HTTPApiDecoder:
# these are the types we can construct (well, only the ones we need)
type_constructables_map = {
Expand Down
141 changes: 141 additions & 0 deletions server/app/interfaces/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""
This module implements the Discovery interface defined in the 'Specification of the Asset Administration Shell Part 2 – Application Programming Interface'.
"""
import json
from typing import Dict, List, Set

import werkzeug.exceptions
from werkzeug.routing import Rule, Submount
from werkzeug.wrappers import Request, Response

from basyx.aas import model
from app.util.converters import IdentifierToBase64URLConverter
from app.interfaces.base import BaseWSGIApp, HTTPApiDecoder
from app import model as server_model
from app.adapter import jsonization


class DiscoveryStore:
def __init__(self):
self.aas_id_to_asset_ids: Dict[model.Identifier, Set[model.SpecificAssetId]] = {}
self.asset_id_to_aas_ids: Dict[model.SpecificAssetId, Set[model.Identifier]] = {}

def get_all_specific_asset_ids_by_aas_id(self, aas_id: model.Identifier) -> List[model.SpecificAssetId]:
return list(self.aas_id_to_asset_ids.get(aas_id, set()))

def add_specific_asset_ids_to_aas(self, aas_id: model.Identifier,
asset_ids: List[model.SpecificAssetId]) -> None:

if aas_id not in self.aas_id_to_asset_ids:
self.aas_id_to_asset_ids[aas_id] = set()

for asset in asset_ids:
self.aas_id_to_asset_ids[aas_id].add(asset)

def delete_specific_asset_ids_by_aas_id(self, aas_id: model.Identifier) -> None:
key = aas_id
if key in self.aas_id_to_asset_ids:
del self.aas_id_to_asset_ids[key]

def search_aas_ids_by_asset_link(self, asset_link: server_model.AssetLink) -> List[model.Identifier]:
result = []
for asset_key, aas_ids in self.asset_id_to_aas_ids.items():
expected_key = f"{asset_link.name}:{asset_link.value}"
if asset_key == expected_key:
result.extend(list(aas_ids))
return result

def _add_aas_id_to_specific_asset_id(self, asset_id: model.SpecificAssetId, aas_id: model.Identifier) -> None:
if asset_id in self.asset_id_to_aas_ids:
self.asset_id_to_aas_ids[asset_id].add(aas_id)
else:
self.asset_id_to_aas_ids[asset_id] = {aas_id}

def _delete_aas_id_from_specific_asset_ids(self, asset_id: model.SpecificAssetId, aas_id: model.Identifier) -> None:
if asset_id in self.asset_id_to_aas_ids:
self.asset_id_to_aas_ids[asset_id].discard(aas_id)

@classmethod
def from_file(cls, filename: str) -> "DiscoveryStore":
"""
Load the state of the `DiscoveryStore` from a local file.
Safely handles files that are missing expected keys.

"""
with open(filename, "r") as file:
data = json.load(file, cls=jsonization.ServerAASFromJsonDecoder)
discovery_store = DiscoveryStore()
discovery_store.aas_id_to_asset_ids = data.get("aas_id_to_asset_ids", {})
discovery_store.asset_id_to_aas_ids = data.get("asset_id_to_aas_ids", {})
return discovery_store

def to_file(self, filename: str) -> None:
"""
Write the current state of the `DiscoveryStore` to a local JSON file for persistence.
"""
with open(filename, "w") as file:
data = {
"aas_id_to_asset_ids": self.aas_id_to_asset_ids,
"asset_id_to_aas_ids": self.asset_id_to_aas_ids,
}
json.dump(data, file, cls=jsonization.ServerAASToJsonEncoder, indent=4)


class DiscoveryAPI(BaseWSGIApp):
def __init__(self,
persistent_store: DiscoveryStore, base_path: str = "/api/v3.1.1"):
self.persistent_store: DiscoveryStore = persistent_store
self.url_map = werkzeug.routing.Map([
Submount(base_path, [
Rule("/lookup/shellsByAssetLink", methods=["POST"],
endpoint=self.search_all_aas_ids_by_asset_link),
Submount("/lookup/shells", [
Rule("/<base64url:aas_id>", methods=["GET"],
endpoint=self.get_all_specific_asset_ids_by_aas_id),
Rule("/<base64url:aas_id>", methods=["POST"],
endpoint=self.post_all_asset_links_by_id),
Rule("/<base64url:aas_id>", methods=["DELETE"],
endpoint=self.delete_all_asset_links_by_id),
]),
])
], converters={
"base64url": IdentifierToBase64URLConverter
}, strict_slashes=False)

def search_all_aas_ids_by_asset_link(self, request: Request, url_args: dict, response_t: type,
**_kwargs) -> Response:
asset_links = HTTPApiDecoder.request_body_list(request, server_model.AssetLink, False)
matching_aas_keys = set()
for asset_link in asset_links:
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)

def get_all_specific_asset_ids_by_aas_id(self, request: Request, url_args: dict, response_t: type, **_kwargs) -> Response:
aas_identifier = str(url_args["aas_id"])
asset_ids = self.persistent_store.get_all_specific_asset_ids_by_aas_id(aas_identifier)
return response_t(asset_ids)

def post_all_asset_links_by_id(self, request: Request, url_args: dict, response_t: type, **_kwargs) -> Response:
aas_identifier = str(url_args["aas_id"])
specific_asset_ids = HTTPApiDecoder.request_body_list(request, model.SpecificAssetId, False)
self.persistent_store.add_specific_asset_ids_to_aas(aas_identifier, specific_asset_ids)
for asset_id in specific_asset_ids:
self.persistent_store._add_aas_id_to_specific_asset_id(asset_id, aas_identifier)
updated = {aas_identifier: self.persistent_store.get_all_specific_asset_ids_by_aas_id(aas_identifier)}
return response_t(updated)

def delete_all_asset_links_by_id(self, request: Request, url_args: dict, response_t: type, **_kwargs) -> Response:
aas_identifier = str(url_args["aas_id"])
self.persistent_store.delete_specific_asset_ids_by_aas_id(aas_identifier)
for key in list(self.persistent_store.asset_id_to_aas_ids.keys()):
self.persistent_store.asset_id_to_aas_ids[key].discard(aas_identifier)
return response_t()


if __name__ == "__main__":
from werkzeug.serving import run_simple

run_simple("localhost", 8084, DiscoveryAPI(DiscoveryStore()),
use_debugger=True, use_reloader=True)
Loading
Loading