Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 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
06e51e7
adapt server repository structure
Frosty2500 Jul 1, 2025
9d43b69
add MyPy checking, remove codeblock checking
Frosty2500 Jul 1, 2025
b6a588e
fix ci pipeline
Frosty2500 Jul 1, 2025
de49b3f
adapt static-analysis
Frosty2500 Jul 1, 2025
db4e42e
docstrings converter.py
Frosty2500 Jul 6, 2025
70d7198
adapt imports, ci; ignore tests for now
Frosty2500 Jul 6, 2025
5962862
Use dir() instead of vars() in `Referable.update_from()` (#338)
zrgt May 26, 2025
3bf7834
sdk/docs: Move documentation dependencies to pyproject.toml (#389)
moritzsommer May 26, 2025
d52d66d
fix MyPy issues
Frosty2500 Jul 6, 2025
4502c9f
fix versioning
Frosty2500 Jul 6, 2025
8f2ef43
adapt lxml, ignore version file
Frosty2500 Jul 6, 2025
1721743
adapt versioning
Frosty2500 Jul 6, 2025
c19765d
exclude build from MyPy, adapt ci
Frosty2500 Jul 10, 2025
168f691
adapt ci
Frosty2500 Jul 10, 2025
7e7adfa
add lxml-stubs
Frosty2500 Jul 10, 2025
d8b3015
clarify Base64URLConverter
Frosty2500 Jul 10, 2025
9bb9067
Add missing newline character at the end of `server/app/pyproject.toml`
s-heppner Jul 15, 2025
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
50 changes: 25 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,7 @@ jobs:
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install .
pip install -r docs/add-requirements.txt
pip install .[docs]
- name: Check documentation for errors
run: |
SPHINXOPTS="-a -E -n -W --keep-going" make -C docs html
Expand Down Expand Up @@ -255,13 +254,13 @@ jobs:
pip install .[dev]
- name: Check typing with MyPy
run: |
mypy ./aas_compliance_tool test
mypy aas_compliance_tool test
- name: Check code style with PyCodestyle
run: |
pycodestyle --count --max-line-length 120 ./aas_compliance_tool test
pycodestyle --count --max-line-length 120 aas_compliance_tool test

compliance-tool-readme-codeblocks:
# This job runs the same static code analysis (mypy and pycodestyle) on the codeblocks in our docstrings.
compliance-tool-package:
# This job checks if we can build our compliance_tool package
runs-on: ubuntu-latest

defaults:
Expand All @@ -273,42 +272,43 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ env.X_PYTHON_MIN_VERSION }}
- name: Install Python dependencies
# install the local sdk in editable mode so it does not get overwritten
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ../sdk[dev]
pip install .[dev]
- name: Check typing with MyPy
run: |
mypy <(codeblocks python README.md)
- name: Check code style with PyCodestyle
run: |
codeblocks --wrap python README.md | pycodestyle --count --max-line-length 120 -
- name: Run readme codeblocks with Python
pip install build
- name: Create source and wheel dist
run: |
codeblocks python README.md | python
python -m build

compliance-tool-package:
# This job checks if we can build our compliance_tool package
#server-test:
# TODO: This job runs the unittests on the python versions specified down at the matrix
# and aas-test-engines on the server


server-static-analysis:
# This job runs static code analysis, namely pycodestyle and mypy
runs-on: ubuntu-latest

defaults:
run:
working-directory: ./compliance_tool
working-directory: ./server/app
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ env.X_PYTHON_MIN_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.X_PYTHON_MIN_VERSION }}
- name: Install dependencies
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Create source and wheel dist
pip install ../../sdk
pip install .[dev]
- name: Check typing with MyPy
run: |
python -m build
mypy .
- name: Check code style with PyCodestyle
run: |
pycodestyle --count --max-line-length 120 .

server-package:
# This job checks if we can build our server package
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ sdk/test/adapter/schemas
# Ignore dynamically generated version file
sdk/basyx/version.py
compliance_tool/aas_compliance_tool/version.py
server/app/version.py

# ignore the content of the server storage
server/storage/
3 changes: 2 additions & 1 deletion sdk/.readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ python:
install:
- method: pip
path: .
- requirements: docs/add-requirements.txt
extra_requirements:
- docs
3 changes: 1 addition & 2 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ The BaSyx Python SDK requires the following Python packages to be installed for
* `lxml` (BSD 3-clause License, using `libxml2` under MIT License)
* `python-dateutil` (BSD 3-clause License)
* `pyecma376-2` (Apache License v2.0)
* `urllib3` (MIT License)
* `Werkzeug` (BSD 3-clause License)


Development/testing/documentation/example dependencies:
* `mypy` (MIT License)
Expand Down
7 changes: 7 additions & 0 deletions sdk/basyx/aas/adapter/_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
PathOrBinaryIO = Union[Path, BinaryIO]
PathOrIO = Union[Path, IO] # IO is TextIO or BinaryIO

# JSON top-level keys and their corresponding model classes
JSON_AAS_TOP_LEVEL_KEYS_TO_TYPES = (
('assetAdministrationShells', model.AssetAdministrationShell),
('submodels', model.Submodel),
('conceptDescriptions', model.ConceptDescription),
)

# XML Namespace definition
XML_NS_MAP = {"aas": "https://admin-shell.io/aas/3/0"}
XML_NS_AAS = "{" + XML_NS_MAP["aas"] + "}"
Expand Down
79 changes: 46 additions & 33 deletions sdk/basyx/aas/adapter/json/json_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@
import json
import logging
import pprint
from typing import Dict, Callable, ContextManager, TypeVar, Type, List, IO, Optional, Set, get_args
from typing import (Dict, Callable, ContextManager, TypeVar, Type,
List, IO, Optional, Set, get_args, Tuple, Iterable, Any)
Comment on lines +37 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you import this many, it is nicer and more readable imo, to put each of them in their own line:

from typing import (
    Dict, 
    Callable,
    ...,
    Any,
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could but we do not do so in the rest of the project. So do you think we should from now always write each in their own line/ change the other imports in the project? I think the current style is also quite readable already.


from basyx.aas import model
from .._generic import MODELLING_KIND_INVERSE, ASSET_KIND_INVERSE, KEY_TYPES_INVERSE, ENTITY_TYPES_INVERSE, \
IEC61360_DATA_TYPES_INVERSE, IEC61360_LEVEL_TYPES_INVERSE, KEY_TYPES_CLASSES_INVERSE, REFERENCE_TYPES_INVERSE, \
DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE, PathOrIO, Path
DIRECTION_INVERSE, STATE_OF_EVENT_INVERSE, QUALIFIER_KIND_INVERSE, PathOrIO, Path, JSON_AAS_TOP_LEVEL_KEYS_TO_TYPES
Comment on lines 41 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. One import per line with ()


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -154,19 +155,20 @@ def __init__(self, *args, **kwargs):
json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)

@classmethod
def object_hook(cls, dct: Dict[str, object]) -> object:
# Check if JSON object seems to be a deserializable AAS object (i.e. it has a modelType). Otherwise, the JSON
# object is returned as is, so it's possible to mix AAS objects with other data within a JSON structure.
if 'modelType' not in dct:
return dct
def _get_aas_class_parsers(cls) -> Dict[str, Callable[[Dict[str, object]], object]]:
"""
Returns the dictionary of AAS class parsers.

The following dict specifies a constructor method for all AAS classes that may be identified using the
``modelType`` attribute in their JSON representation. Each of those constructor functions takes the JSON
representation of an object and tries to construct a Python object from it. Embedded objects that have a
modelType themselves are expected to be converted to the correct PythonType already. Additionally, each
function takes a bool parameter ``failsafe``, which indicates weather to log errors and skip defective objects
instead of raising an Exception.

# The following dict specifies a constructor method for all AAS classes that may be identified using the
# ``modelType`` attribute in their JSON representation. Each of those constructor functions takes the JSON
# representation of an object and tries to construct a Python object from it. Embedded objects that have a
# modelType themselves are expected to be converted to the correct PythonType already. Additionally, each
# function takes a bool parameter ``failsafe``, which indicates weather to log errors and skip defective objects
# instead of raising an Exception.
AAS_CLASS_PARSERS: Dict[str, Callable[[Dict[str, object]], object]] = {
:return: The dictionary of AAS class parsers
"""
aas_class_parsers: Dict[str, Callable[[Dict[str, object]], object]] = {
'AssetAdministrationShell': cls._construct_asset_administration_shell,
'AssetInformation': cls._construct_asset_information,
'SpecificAssetId': cls._construct_specific_asset_id,
Expand All @@ -189,6 +191,16 @@ def object_hook(cls, dct: Dict[str, object]) -> object:
'ReferenceElement': cls._construct_reference_element,
'DataSpecificationIec61360': cls._construct_data_specification_iec61360,
}
return aas_class_parsers

@classmethod
def object_hook(cls, dct: Dict[str, object]) -> object:
# Check if JSON object seems to be a deserializable AAS object (i.e. it has a modelType). Otherwise, the JSON
# object is returned as is, so it's possible to mix AAS objects with other data within a JSON structure.
if 'modelType' not in dct:
return dct

AAS_CLASS_PARSERS = cls._get_aas_class_parsers()

# Get modelType and constructor function
if not isinstance(dct['modelType'], str):
Expand Down Expand Up @@ -799,7 +811,9 @@ def _select_decoder(failsafe: bool, stripped: bool, decoder: Optional[Type[AASFr

def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: PathOrIO, replace_existing: bool = False,
ignore_existing: bool = False, failsafe: bool = True, stripped: bool = False,
decoder: Optional[Type[AASFromJsonDecoder]] = None) -> Set[model.Identifier]:
decoder: Optional[Type[AASFromJsonDecoder]] = None,
keys_to_types: Iterable[Tuple[str, Any]] = JSON_AAS_TOP_LEVEL_KEYS_TO_TYPES) \
-> Set[model.Identifier]:
"""
Read an Asset Administration Shell JSON file according to 'Details of the Asset Administration Shell', chapter 5.5
into a given object store.
Expand All @@ -817,6 +831,7 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: PathO
See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91
This parameter is ignored if a decoder class is specified.
:param decoder: The decoder class used to decode the JSON objects
:param keys_to_types: A dictionary of JSON keys to expected types. This is used to check the type of the objects
:raises KeyError: **Non-failsafe**: Encountered a duplicate identifier
:raises KeyError: Encountered an identifier that already exists in the given ``object_store`` with both
``replace_existing`` and ``ignore_existing`` set to ``False``
Expand All @@ -843,45 +858,43 @@ def read_aas_json_file_into(object_store: model.AbstractObjectStore, file: PathO
with cm as fp:
data = json.load(fp, cls=decoder_)

for name, expected_type in (('assetAdministrationShells', model.AssetAdministrationShell),
('submodels', model.Submodel),
('conceptDescriptions', model.ConceptDescription)):
for name, expected_type in keys_to_types:
try:
lst = _get_ts(data, name, list)
except (KeyError, TypeError):
continue

for item in lst:
error_message = "Expected a {} in list '{}', but found {}".format(
expected_type.__name__, name, repr(item))
error_msg = f"Expected a {expected_type.__name__} in list '{name}', but found {repr(item)}."
if isinstance(item, model.Identifiable):
if not isinstance(item, expected_type):
if decoder_.failsafe:
logger.warning("{} was in wrong list '{}'; nevertheless, we'll use it".format(item, name))
else:
raise TypeError(error_message)
if not decoder_.failsafe:
raise TypeError(f"{item} was in the wrong list '{name}'")
logger.warning(f"{item} was in the wrong list '{name}'; nevertheless, we'll use it")

if item.id in ret:
error_message = f"{item} has a duplicate identifier already parsed in the document!"
error_msg = f"{item} has a duplicate identifier already parsed in the document!"
if not decoder_.failsafe:
raise KeyError(error_message)
logger.error(error_message + " skipping it...")
raise KeyError(error_msg)
logger.error(f"{error_msg} Skipping it...")
continue

existing_element = object_store.get(item.id)
if existing_element is not None:
if not replace_existing:
error_message = f"object with identifier {item.id} already exists " \
f"in the object store: {existing_element}!"
error_msg = f"Object with id '{item.id}' already exists in store: {existing_element}!"
if not ignore_existing:
raise KeyError(error_message + f" failed to insert {item}!")
logger.info(error_message + f" skipping insertion of {item}...")
raise KeyError(f"{error_msg} Failed to insert {item}!")
logger.info(f"{error_msg} Skipping {item}...")
continue
object_store.discard(existing_element)

object_store.add(item)
ret.add(item.id)
elif decoder_.failsafe:
logger.error(error_message)
logger.error(f"{error_msg} Skipping it...")
else:
raise TypeError(error_message)
raise TypeError(error_msg)
return ret


Expand Down
Loading